Commit | Line | Data |
daa0fd7d |
1 | package Moose::Cookbook::Meta::Recipe3; |
aff0421c |
2 | |
daa0fd7d |
3 | # ABSTRACT: Labels implemented via attribute traits |
4 | |
5 | __END__ |
aff0421c |
6 | |
aff0421c |
7 | |
daa0fd7d |
8 | =pod |
aff0421c |
9 | |
10 | =head1 SYNOPSIS |
11 | |
6a7e3999 |
12 | package MyApp::Meta::Attribute::Trait::Labeled; |
13 | use Moose::Role; |
14 | |
15 | has label => ( |
16 | is => 'rw', |
17 | isa => 'Str', |
18 | predicate => 'has_label', |
19 | ); |
20 | |
21 | package Moose::Meta::Attribute::Custom::Trait::Labeled; |
22 | sub register_implementation {'MyApp::Meta::Attribute::Trait::Labeled'} |
23 | |
24 | package MyApp::Website; |
25 | use Moose; |
6a7e3999 |
26 | |
27 | has url => ( |
28 | traits => [qw/Labeled/], |
29 | is => 'rw', |
30 | isa => 'Str', |
31 | label => "The site's URL", |
32 | ); |
33 | |
34 | has name => ( |
35 | is => 'rw', |
36 | isa => 'Str', |
37 | ); |
38 | |
39 | sub dump { |
40 | my $self = shift; |
41 | |
ce444596 |
42 | my $meta = $self->meta; |
43 | |
c79239a2 |
44 | my $dump = ''; |
45 | |
ce444596 |
46 | for my $attribute ( map { $meta->get_attribute($_) } |
47 | sort $meta->get_attribute_list ) { |
6a7e3999 |
48 | |
6a7e3999 |
49 | if ( $attribute->does('MyApp::Meta::Attribute::Trait::Labeled') |
50 | && $attribute->has_label ) { |
c79239a2 |
51 | $dump .= $attribute->label; |
6a7e3999 |
52 | } |
6a7e3999 |
53 | else { |
ce444596 |
54 | $dump .= $attribute->name; |
6a7e3999 |
55 | } |
56 | |
6a7e3999 |
57 | my $reader = $attribute->get_read_method; |
c79239a2 |
58 | $dump .= ": " . $self->$reader . "\n"; |
6a7e3999 |
59 | } |
c79239a2 |
60 | |
61 | return $dump; |
6a7e3999 |
62 | } |
63 | |
64 | package main; |
c79239a2 |
65 | |
6a7e3999 |
66 | my $app = MyApp::Website->new( url => "http://google.com", name => "Google" ); |
aff0421c |
67 | |
68 | =head1 BUT FIRST |
69 | |
4515e88e |
70 | This recipe is a variation on |
09412052 |
71 | L<Moose::Cookbook::Meta::Recipe2>. Please read that recipe first. |
aff0421c |
72 | |
73 | =head1 MOTIVATION |
74 | |
09412052 |
75 | In L<Moose::Cookbook::Meta::Recipe2>, we created an attribute |
4515e88e |
76 | metaclass which lets you provide a label for attributes. |
77 | |
19320607 |
78 | Using a metaclass works fine until you realize you want to add a label |
4515e88e |
79 | I<and> an expiration, or some other combination of new behaviors. You |
80 | could create yet another metaclass which subclasses those two, but |
81 | that makes a mess, especially if you want to mix and match behaviors |
82 | across many attributes. |
83 | |
84 | Fortunately, Moose provides a much saner alternative, which is to |
85 | encapsulate each extension as a role, not a class. We can make a role |
86 | which adds a label to an attribute, and could make another to |
87 | implement expiration. |
aff0421c |
88 | |
89 | =head1 TRAITS |
90 | |
4515e88e |
91 | Roles that apply to metaclasses have a special name: traits. Don't let |
92 | the change in nomenclature fool you, B<traits are just roles>. |
aff0421c |
93 | |
4515e88e |
94 | L<Moose/has> allows you to pass a C<traits> parameter for an |
95 | attribute. This parameter takes a list of trait names which are |
96 | composed into an anonymous metaclass, and that anonymous metaclass is |
97 | used for the attribute. |
aff0421c |
98 | |
4515e88e |
99 | Yes, we still have lots of metaclasses in the background, but they're |
100 | managed by Moose for you. |
101 | |
102 | Traits can do anything roles can do. They can add or refine |
103 | attributes, wrap methods, provide more methods, define an interface, |
104 | etc. The only difference is that you're now changing the attribute |
105 | metaclass instead of a user-level class. |
aff0421c |
106 | |
107 | =head1 DISSECTION |
108 | |
4515e88e |
109 | A side-by-side look of the code examples in this recipe and recipe 2 |
110 | show that defining and using a trait is very similar to a full-blown |
111 | metaclass. |
aff0421c |
112 | |
6a7e3999 |
113 | package MyApp::Meta::Attribute::Trait::Labeled; |
114 | use Moose::Role; |
aff0421c |
115 | |
6a7e3999 |
116 | has label => ( |
117 | is => 'rw', |
118 | isa => 'Str', |
119 | predicate => 'has_label', |
120 | ); |
aff0421c |
121 | |
4515e88e |
122 | Instead of subclassing L<Moose::Meta::Attribute>, we define a role. As |
123 | with our metaclass in L<recipe 2|Moose::Cookbook::Meta::Recipe2>, |
124 | registering our role allows us to refer to it by a short name. |
aff0421c |
125 | |
6a7e3999 |
126 | package Moose::Meta::Attribute::Custom::Trait::Labeled; |
127 | sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' } |
aff0421c |
128 | |
4515e88e |
129 | Moose looks for the C<register_implementation> method in |
aff0421c |
130 | C<Moose::Meta::Attribute::Custom::Trait::$TRAIT_NAME> to find the full |
131 | name of the trait. |
132 | |
4515e88e |
133 | For the rest of the code, we will only cover what is I<different> from |
134 | L<recipe 2|Moose::Cookbook::Meta::Recipe2>. |
aff0421c |
135 | |
6a7e3999 |
136 | has url => ( |
137 | traits => [qw/Labeled/], |
138 | is => 'rw', |
139 | isa => 'Str', |
140 | label => "The site's URL", |
141 | ); |
aff0421c |
142 | |
4515e88e |
143 | Instead of passing a C<metaclass> parameter, this time we pass |
144 | C<traits>. This contains a list of trait names. Moose will build an |
145 | anonymous attribute metaclass from these traits and use it for this |
146 | attribute. Passing a C<label> parameter works just as it did with the |
147 | metaclass example. |
aff0421c |
148 | |
c79239a2 |
149 | if ( $attribute->does('MyApp::Meta::Attribute::Trait::Labeled') |
150 | && $attribute->has_label ) { |
151 | $dump .= $attribute->label; |
152 | } |
aff0421c |
153 | |
4515e88e |
154 | In the metaclass example, we used C<< $attribute->isa >>. With a role, |
155 | we instead ask if the meta-attribute object C<does> the required |
156 | role. If it does not do this role, the attribute meta object won't |
157 | have the C<has_label> method. |
aff0421c |
158 | |
159 | That's all. Everything else is the same! |
160 | |
4515e88e |
161 | =head1 TURNING A METACLASS INTO A TRAIT |
d9a8643f |
162 | |
163 | "But wait!" you protest. "I've already written all of my extensions as |
164 | attribute metaclasses. I don't want to break all that code out there." |
165 | |
4515e88e |
166 | Fortunately, you can easily turn a metaclass into a trait and still |
167 | provide the original metaclass: |
d9a8643f |
168 | |
6a7e3999 |
169 | package MyApp::Meta::Attribute::Labeled; |
170 | use Moose; |
171 | extends 'Moose::Meta::Attribute'; |
172 | with 'MyApp::Meta::Attribute::Trait::Labeled'; |
d9a8643f |
173 | |
6a7e3999 |
174 | package Moose::Meta::Attribute::Custom::Labeled; |
175 | sub register_implementation { 'MyApp::Meta::Attribute::Labeled' } |
d9a8643f |
176 | |
4515e88e |
177 | Unfortunately, going the other way (providing a trait created from a |
178 | metaclass) is more tricky. |
d9a8643f |
179 | |
aff0421c |
180 | =head1 CONCLUSION |
181 | |
4515e88e |
182 | If you're extending your attributes, it's easier and more flexible to |
183 | provide composable bits of behavior than to subclass |
184 | L<Moose::Meta::Attribute>. Using traits lets you cooperate with other |
185 | extensions, either from CPAN or that you might write in the |
186 | future. Moose makes it easy to create attribute metaclasses on the fly |
187 | by providing a list of trait names to L<Moose/has>. |
aff0421c |
188 | |
c79239a2 |
189 | =begin testing |
aff0421c |
190 | |
c79239a2 |
191 | my $app2 |
192 | = MyApp::Website->new( url => "http://google.com", name => "Google" ); |
193 | is( |
194 | $app2->dump, q{name: Google |
195 | The site's URL: http://google.com |
196 | }, '... got the expected dump value' |
197 | ); |
aff0421c |
198 | |
c79239a2 |
199 | |
200 | =end testing |
201 | |
202 | =cut |