NAME

Moose::Cookbook::Recipe22 - The attribute trait example

SYNOPSIS

    package MyApp::Meta::Attribute::Trait::Labeled;
    use Moose::Role;

    has label => (
        is        => 'rw',
        isa       => 'Str',
        predicate => 'has_label',
    );

    package Moose::Meta::Attribute::Custom::Trait::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' }

    package MyApp::Website;
    use Moose;
    use MyApp::Meta::Attribute::Trait::Labeled;

    has url => (
        traits => [qw/Labeled/],
        is     => 'rw',
        isa    => 'Str',
        label  => "The site's URL",
    );

    has name => (
        is  => 'rw',
        isa => 'Str',
    );

    sub dump {
        my $self = shift;

        # iterate over all the attributes in $self
        my %attributes = %{ $self->meta->get_attribute_map };
        while (my ($name, $attribute) = each %attributes) {

            # print the label if available
            if ($attribute->does('MyApp::Meta::Attribute::Trait::Labeled')
                && $attribute->has_label) {
                    print $attribute->label;
            }
            # otherwise print the name
            else {
                print $name;
            }

            # print the attribute's value
            my $reader = $attribute->get_read_method;
            print ": " . $self->$reader . "\n";
        }
    }

    package main;
    my $app = MyApp::Website->new(url => "http://google.com", name => "Google");
    $app->dump;

BUT FIRST

This recipe is a continuation of Moose::Cookbook::Recipe21. Please read that first.

MOTIVATION

In Recipe 21, we created an attribute metaclass that gives attributes a "label" that can be set in "has" in Moose. That works well until you want a second meta-attribute, or until you want to adjust the behavior of the attribute. You could define a specialized attribute metaclass to use in every attribute. However, you may want different attributes to have different behaviors. You might end up with a unique attribute metaclass for every single attribute, with a lot of code copying and pasting!

Or, if you've been drinking deeply of the Moose kool-aid, you'll have a role for each of the behaviors. One role would give a label meta-attribute. Another role would signify that this attribute is not directly modifiable via the REST interface. Another role would write to a logfile when this attribute was read.

Unfortunately, you'd still be left with a bunch of attribute metaclasses that do nothing but compose a bunch of roles. If only there were some way to specify in "has" in Moose a list of roles to apply to the attribute metaclass...

TRAITS

Roles that apply to metaclasses have a special name: traits. Don't let the change in nomenclature fool you, traits are just roles.

"has" in Moose provides a traits option. It takes a list of trait names to compose into an anonymous metaclass. That means you do still have a bunch of attribute metaclasses that do nothing but compose a bunch of roles, but they're managed automatically by Moose. You don't need to declare them in advance, or worry whether changing one will affect some other attribute.

What can traits do? Anything roles can do. They can add or refine attributes, wrap methods, provide more methods, define an interface, etc. The only difference is that you're now changing the attribute metaclass instead of a user-level class.

DISSECTION

A side-by-side look of the code examples in this recipe and recipe 21 should indicate that defining and using a trait is very similar to defining and using a new attribute metaclass.

    package MyApp::Meta::Attribute::Trait::Labeled;
    use Moose::Role;

    has label => (
        is        => 'rw',
        isa       => 'Str',
        predicate => 'has_label',
    );

Instead of subclassing Moose::Meta::Attribute, we define a role. Traits don't need any special methods or attributes. You just focus on whatever it is you actually need to get done. Here we're adding a new meta-attribute for use in our application.

    package Moose::Meta::Attribute::Custom::Trait::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' }

Much like when we define a new attribute metaclass, we can provide a shorthand name for the trait. Moose looks at the register_implementation method in Moose::Meta::Attribute::Custom::Trait::$TRAIT_NAME to find the full name of the trait.

Now we begin writing our application logic. I'll only cover what has changed since recipe 21.

    has url => (
        traits => [qw/Labeled/],
        is     => 'rw',
        isa    => 'Str',
        label  => "The site's URL",
    );

"has" in Moose provides a traits option. Just pass the list of trait names and it will compose them together to form the (anonymous) attribute metaclass used by the attribute. We provide a label for the attribute in the same way.

    # print the label if available
    if ($attribute->does('MyApp::Meta::Attribute::Trait::Labeled')
        && $attribute->has_label) {
            print $attribute->label;
    }

Previously, this code asked the question "Does this attribute use our attribute metaclass?" Since we're now using a trait, we ask "Does this attribute's metaclass do the Labeled role?" If not, the attribute metaclass won't have the has_label method, and so it would be an error to blindly call $attribute->has_label.

That's all. Everything else is the same!

METACLASS + TRAIT

"But wait!" you protest. "I've already written all of my extensions as attribute metaclasses. I don't want to break all that code out there."

All is not lost. If you rewrite your extension as a trait, then you can easily get a regular metaclass extension out of it. You just compose the trait in the attribute metaclass, as normal.

    package MyApp::Meta::Attribute::Labeled;
    use Moose;
    extends 'Moose::Meta::Attribute';
    with 'MyApp::Meta::Attribute::Trait::Labeled';

    package Moose::Meta::Attribute::Custom::Labeled;
    sub register_implementation { 'MyApp::Meta::Attribute::Labeled' }

Unfortunately, going the other way (providing a trait created from a metaclass) is more tricky. Thus, defining your extensions as traits is just plain better than defining them as subclassed metaclasses.

CONCLUSION

If you're extending your attributes, it's easier and more flexible to provide composable bits of behavior than to subclass Moose::Meta::Attribute. Using traits (which are just roles applied to a metaclass!) let you choose exactly which behaviors each attribute will have. Moose makes it easy to create attribute metaclasses on the fly by providing a list of trait names to "has" in Moose.

AUTHOR

Shawn M Moore <sartak@gmail.com>

COPYRIGHT AND LICENSE

Copyright 2006-2008 by Infinity Interactive, Inc.

http://www.iinteractive.com

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.