A few more recipe 11 cleanups and rephrasings
[gitmo/Moose.git] / lib / Moose / Cookbook / Recipe11.pod
CommitLineData
8323a774 1=head1 NAME
2
3Moose::Cookbook::Recipe11 - The meta-attribute example
4
8d8832a4 5=head1 SYNOPSIS
6
8323a774 7 package MyApp::Meta::Attribute::Labeled;
8 use Moose;
9 extends 'Moose::Meta::Attribute';
10
05ddeca1 11 has label => (
94acbcd7 12 is => 'rw',
05ddeca1 13 isa => 'Str',
8323a774 14 predicate => 'has_label',
05ddeca1 15 );
8323a774 16
17 package Moose::Meta::Attribute::Custom::Labeled;
18 sub register_implementation { 'MyApp::Meta::Attribute::Labeled' }
19
20 package MyApp::Website;
21 use Moose;
22 use MyApp::Meta::Attribute::Labeled;
23
24 has url => (
25 metaclass => 'Labeled',
26 isa => 'Str',
27 is => 'rw',
28 label => "The site's URL",
29 );
30
31 has name => (
32 is => 'rw',
33 isa => 'Str',
34 );
35
36 sub dump {
37 my $self = shift;
38
39 # iterate over all the attributes in $self
40 my %attributes = %{ $self->meta->get_attribute_map };
0cd28189 41 while (my ($name, $attribute) = each %attributes) {
8323a774 42
43 # print the label if available
0cd28189 44 if ($attribute->isa('MyApp::Meta::Attribute::Labeled')
45 && $attribute->has_label) {
46 print $attribute->label;
8323a774 47 }
48 # otherwise print the name
49 else {
50 print $name;
51 }
52
53 # print the attribute's value
0cd28189 54 my $reader = $attribute->get_read_method;
8323a774 55 print ": " . $self->$reader . "\n";
56 }
57 }
58
59 package main;
60 my $app = MyApp::Website->new(url => "http://google.com", name => "Google");
61 $app->dump;
62
63=head1 SUMMARY
64
65In this recipe, we begin to really delve into the wonder of meta-programming.
66Some readers may scoff and claim that this is the arena only of the most
67twisted Moose developers. Absolutely not! Any sufficiently twisted developer
68can benefit greatly from going more meta.
69
70The high-level goal of this recipe's code is to allow each attribute to have a
71human-readable "label" attached to it. Such labels would be used when showing
72data to an end user. In this recipe we label the "url" attribute with "The
73site's URL" and create a simple method to demonstrate how to use that label.
74
75=head1 REAL ATTRIBUTES 101
76
77All the attributes of a Moose-based object are actually objects themselves.
78These objects have methods and (surprisingly) attributes. Let's look at a
79concrete example.
80
81 has 'x' => (isa => 'Int', is => 'ro');
82 has 'y' => (isa => 'Int', is => 'rw');
83
84Ahh, the veritable x and y of the Point example. Internally, every Point has an
85x object and a y object. They have methods (such as "get_value") and attributes
86(such as "is_lazy"). What class are they instances of?
87L<Moose::Meta::Attribute>. You don't normally see the objects lurking behind
88the scenes, because you usually just use C<< $point->x >> and C<< $point->y >>
89and forget that there's a lot of machinery lying in such methods.
90
91So you have a C<$point> object, which has C<x> and C<y> methods. How can you
92actually access the objects behind these attributes? Here's one way:
93
94 $point->meta->get_attribute_map()
95
96C<get_attribute_map> returns a hash reference that maps attribute names to
97their objects. In our case, C<get_attribute_map> might return something that
98looks like the following:
99
100 {
101 x => Moose::Meta::Attribute=HASH(0x196c23c),
102 y => Moose::Meta::Attribute=HASH(0x18d1690),
103 }
104
376a56f7 105Another way to get a handle on an attribute's object is
106C<< $self->meta->get_attribute('name') >>. Here's one thing you can do now that
107you can interact with the attribute's object directly:
8323a774 108
376a56f7 109 print $point->meta->get_attribute('x')->type_constraint;
110 => Int
8323a774 111
112(As an aside, it's not called C<< ->isa >> because C<< $obj->isa >> is already
113taken)
114
115So to actually beef up attributes, what we need to do is:
116
117=over 4
118
119=item Create a new attribute metaclass
120
121=item Create attributes using that new metaclass
122
123=back
124
125Moose makes both of these easy!
126
127Let's start dissecting the recipe's code.
128
129=head1 DISSECTION
130
131We get the ball rolling by creating a new attribute metaclass. It starts off
132somewhat ungloriously.
133
134 package MyApp::Meta::Attribute::Labeled;
135 use Moose;
136 extends 'Moose::Meta::Attribute';
137
138You subclass metaclasses the same way you subclass regular classes. (Extra
94acbcd7 139credit: how in the actual hell can you use the MOP to extend itself?)
8323a774 140
05ddeca1 141 has label => (
94acbcd7 142 is => 'rw',
05ddeca1 143 isa => 'Str',
8323a774 144 predicate => 'has_label',
05ddeca1 145 );
8323a774 146
94acbcd7 147Hey, this looks pretty reasonable! This is plain jane Moose code. Recipe 1
148fare. This is merely making a new attribute. An attribute that attributes have.
149A meta-attribute. It may sound scary, but it really isn't! Reread
150L<REAL ATTRIBUTES 101> if this really is terrifying.
8323a774 151
94acbcd7 152The name is "label", it will have a regular accessor (except of course at
153create time), and is a tring. C<predicate> is a standard part of C<has>. It
154just creates a method that asks the question "Does this attribute have a
155value?"
8323a774 156
157 package Moose::Meta::Attribute::Custom::Labeled;
158 sub register_implementation { 'MyApp::Meta::Attribute::Labeled' }
159
94acbcd7 160This registers our new metaclass with Moose. That way attributes can actually
8323a774 161use it. More on what this is doing in a moment.
162
163Note that we're done defining the new metaclass! Only nine lines of code, and
164not particularly difficult lines, either. Now to start using the metaclass.
165
166 package MyApp::Website;
167 use Moose;
168 use MyApp::Meta::Attribute::Labeled;
169
170Nothing new here. We do have to actually load our metaclass to be able to use
171it.
172
173 has url => (
174 metaclass => 'Labeled',
175 isa => 'Str',
176 is => 'rw',
177 label => "The site's URL",
178 );
179
180Ah ha! Now we're using the metaclass. We're adding a new attribute, C<url>, to
dbea36a1 181C<MyApp::Website>. C<has> lets you set the metaclass of the attribute.
182Ordinarily (as we've seen), the metaclass is C<Moose::Meta::Attribute>.
8323a774 183
184When C<has> sees that you're using a new metaclass, it will take the
185metaclass's name, prepend C<Moose::Meta::Attribute::Custom::>, and call the
186C<register_implementation> function in that package. So here Moose calls
dbea36a1 187C<Moose::Meta::Attribute::Custom::Labeled::register_implementation>. We defined
188that function in the beginning -- it just returns our "real" metaclass'
189package, C<MyApp::Meta::Attribute::Labeled>. So Moose uses that metaclass for
190the attribute. It may seem a bit convoluted, but the alternative would be to
191use C<< metaclass => 'MyApp::Meta::Attribute::Labeled' >> on every attribute.
192As usual, Moose optimizes in favor of the end user, not the metaprogrammer. :)
8323a774 193
194Finally, we see that C<has> is setting our new meta-attribute, C<label>, to
94acbcd7 195C<"The site's URL">. We can access this meta-attribute with:
196
197 $website->meta->get_attribute('url')->label()
198
199Back to the code.
8323a774 200
201 has name => (
202 is => 'rw',
203 isa => 'Str',
204 );
205
206You do not of course need to use the new metaclass for all new attributes.
207
208Now we begin defining a method that will dump the C<MyApp::Website> instance
209for human readers.
210
211 sub dump {
212 my $self = shift;
213
214 # iterate over all the attributes in $self
215 my %attributes = %{ $self->meta->get_attribute_map };
0cd28189 216 while (my ($name, $attribute) = each %attributes) {
8323a774 217
94acbcd7 218Recall that C<get_attribute_map> returns a hashref of attribute names and their
219associated objects.
8323a774 220
221 # print the label if available
0cd28189 222 if ($attribute->isa('MyApp::Meta::Attribute::Labeled')
223 && $attribute->has_label) {
224 print $attribute->label;
8323a774 225 }
226
94acbcd7 227We have two checks here. The first is "is this attribute an instance of
228C<MyApp::Meta::Attribute::Labeled>?". It's good to code defensively. Even if
229all of your attributes have this metaclass, you never know when someone is
230going to subclass your work of art. Poorly. The second check is "does this
8323a774 231attribute have a label?". This method was defined in the new metaclass as the
94acbcd7 232"predicate". If we pass both checks, we print the attribute's label.
8323a774 233
234 # otherwise print the name
235 else {
236 print $name;
237 }
238
239Another good, defensive coding practice: Provide reasonable defaults.
240
241 # print the attribute's value
0cd28189 242 my $reader = $attribute->get_read_method;
8323a774 243 print ": " . $self->$reader . "\n";
244 }
245 }
246
94acbcd7 247Here's another example of using the attribute metaclass.
248C<< $attribute->get_read_method >> returns the name of the method that can
249invoked on the original object to read the attribute's value.
250C<< $self->$reader >> is an example of "reflection" -- instead of using the
251name of the method, we're using a variable with the name of the method in it.
252Perl doesn't mind. Another way to write this would be
253C<< $self->can($reader)->($self) >>.
8323a774 254
255 package main;
256 my $app = MyApp::Website->new(url => "http://google.com", name => "Google");
257 $app->dump;
258
94acbcd7 259And wrap up the example with a script to show off our newfound magic.
8323a774 260
261=head1 CONCLUSION
262
263Why oh why would you want to go through all of these contortions when you can
264just print "The site's URL" directly in the C<dump> method? For one, the DRY
265(Don't Repeat Yourself) principle. If you have it in the C<dump> method, you'll
266probably also have it in the C<as_form> method, and C<to_file>, and so on. So
267why not have a method that maps attribute names to labels? That could work, but
268why not include the label where it belongs, in the attribute's definition?
269That way you're also less likely to forget to add the label.
270
271More importantly, this was a very simple example. Your metaclasses aren't
272limited to just adding new meta-attributes. For example, you could implement
273a metaclass that expires attributes after a certain amount of time.
274
275 has site_cache => (
276 metaclass => 'Expiry',
277 expires_after => '1 hour',
12369f85 278 refresh_with => sub { my $self = shift; get($self->url) },
8323a774 279 isa => 'Str',
280 );
281
282The sky's the limit!
283
284=head1 AUTHOR
285
286Shawn M Moore E<lt>sartak@gmail.comE<gt>
287
288=head1 COPYRIGHT AND LICENSE
289
290Copyright 2006, 2007 by Infinity Interactive, Inc.
291
292L<http://www.iinteractive.com>
293
294This library is free software; you can redistribute it and/or modify
295it under the same terms as Perl itself.
296
297=cut
298
2991;
300