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