Commit | Line | Data |
8323a774 |
1 | =head1 NAME |
2 | |
3 | Moose::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 | |
65 | In this recipe, we begin to really delve into the wonder of meta-programming. |
66 | Some readers may scoff and claim that this is the arena only of the most |
67 | twisted Moose developers. Absolutely not! Any sufficiently twisted developer |
68 | can benefit greatly from going more meta. |
69 | |
70 | The high-level goal of this recipe's code is to allow each attribute to have a |
71 | human-readable "label" attached to it. Such labels would be used when showing |
72 | data to an end user. In this recipe we label the "url" attribute with "The |
73 | site's URL" and create a simple method to demonstrate how to use that label. |
74 | |
75 | =head1 REAL ATTRIBUTES 101 |
76 | |
77 | All the attributes of a Moose-based object are actually objects themselves. |
78 | These objects have methods and (surprisingly) attributes. Let's look at a |
79 | concrete example. |
80 | |
81 | has 'x' => (isa => 'Int', is => 'ro'); |
82 | has 'y' => (isa => 'Int', is => 'rw'); |
83 | |
84 | Ahh, the veritable x and y of the Point example. Internally, every Point has an |
85 | x 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? |
87 | L<Moose::Meta::Attribute>. You don't normally see the objects lurking behind |
88 | the scenes, because you usually just use C<< $point->x >> and C<< $point->y >> |
89 | and forget that there's a lot of machinery lying in such methods. |
90 | |
91 | So you have a C<$point> object, which has C<x> and C<y> methods. How can you |
92 | actually access the objects behind these attributes? Here's one way: |
93 | |
94 | $point->meta->get_attribute_map() |
95 | |
96 | C<get_attribute_map> returns a hash reference that maps attribute names to |
97 | their objects. In our case, C<get_attribute_map> might return something that |
98 | looks like the following: |
99 | |
100 | { |
101 | x => Moose::Meta::Attribute=HASH(0x196c23c), |
102 | y => Moose::Meta::Attribute=HASH(0x18d1690), |
103 | } |
104 | |
376a56f7 |
105 | Another way to get a handle on an attribute's object is |
106 | C<< $self->meta->get_attribute('name') >>. Here's one thing you can do now that |
107 | you 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 |
113 | taken) |
114 | |
115 | So 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 | |
125 | Moose makes both of these easy! |
126 | |
127 | Let's start dissecting the recipe's code. |
128 | |
129 | =head1 DISSECTION |
130 | |
131 | We get the ball rolling by creating a new attribute metaclass. It starts off |
132 | somewhat ungloriously. |
133 | |
134 | package MyApp::Meta::Attribute::Labeled; |
135 | use Moose; |
136 | extends 'Moose::Meta::Attribute'; |
137 | |
138 | You subclass metaclasses the same way you subclass regular classes. (Extra |
94acbcd7 |
139 | credit: 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 |
147 | Hey, this looks pretty reasonable! This is plain jane Moose code. Recipe 1 |
148 | fare. This is merely making a new attribute. An attribute that attributes have. |
149 | A meta-attribute. It may sound scary, but it really isn't! Reread |
150 | L<REAL ATTRIBUTES 101> if this really is terrifying. |
8323a774 |
151 | |
94acbcd7 |
152 | The name is "label", it will have a regular accessor (except of course at |
153 | create time), and is a tring. C<predicate> is a standard part of C<has>. It |
154 | just creates a method that asks the question "Does this attribute have a |
155 | value?" |
8323a774 |
156 | |
157 | package Moose::Meta::Attribute::Custom::Labeled; |
158 | sub register_implementation { 'MyApp::Meta::Attribute::Labeled' } |
159 | |
94acbcd7 |
160 | This registers our new metaclass with Moose. That way attributes can actually |
8323a774 |
161 | use it. More on what this is doing in a moment. |
162 | |
163 | Note that we're done defining the new metaclass! Only nine lines of code, and |
164 | not 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 | |
170 | Nothing new here. We do have to actually load our metaclass to be able to use |
171 | it. |
172 | |
173 | has url => ( |
174 | metaclass => 'Labeled', |
175 | isa => 'Str', |
176 | is => 'rw', |
177 | label => "The site's URL", |
178 | ); |
179 | |
180 | Ah ha! Now we're using the metaclass. We're adding a new attribute, C<url>, to |
dbea36a1 |
181 | C<MyApp::Website>. C<has> lets you set the metaclass of the attribute. |
182 | Ordinarily (as we've seen), the metaclass is C<Moose::Meta::Attribute>. |
8323a774 |
183 | |
184 | When C<has> sees that you're using a new metaclass, it will take the |
185 | metaclass's name, prepend C<Moose::Meta::Attribute::Custom::>, and call the |
186 | C<register_implementation> function in that package. So here Moose calls |
dbea36a1 |
187 | C<Moose::Meta::Attribute::Custom::Labeled::register_implementation>. We defined |
188 | that function in the beginning -- it just returns our "real" metaclass' |
189 | package, C<MyApp::Meta::Attribute::Labeled>. So Moose uses that metaclass for |
190 | the attribute. It may seem a bit convoluted, but the alternative would be to |
191 | use C<< metaclass => 'MyApp::Meta::Attribute::Labeled' >> on every attribute. |
192 | As usual, Moose optimizes in favor of the end user, not the metaprogrammer. :) |
8323a774 |
193 | |
194 | Finally, we see that C<has> is setting our new meta-attribute, C<label>, to |
94acbcd7 |
195 | C<"The site's URL">. We can access this meta-attribute with: |
196 | |
197 | $website->meta->get_attribute('url')->label() |
198 | |
199 | Back to the code. |
8323a774 |
200 | |
201 | has name => ( |
202 | is => 'rw', |
203 | isa => 'Str', |
204 | ); |
205 | |
206 | You do not of course need to use the new metaclass for all new attributes. |
207 | |
208 | Now we begin defining a method that will dump the C<MyApp::Website> instance |
209 | for 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 |
218 | Recall that C<get_attribute_map> returns a hashref of attribute names and their |
219 | associated 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 |
227 | We have two checks here. The first is "is this attribute an instance of |
228 | C<MyApp::Meta::Attribute::Labeled>?". It's good to code defensively. Even if |
229 | all of your attributes have this metaclass, you never know when someone is |
230 | going to subclass your work of art. Poorly. The second check is "does this |
8323a774 |
231 | attribute 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 | |
239 | Another 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 |
247 | Here's another example of using the attribute metaclass. |
248 | C<< $attribute->get_read_method >> returns the name of the method that can |
249 | invoked on the original object to read the attribute's value. |
250 | C<< $self->$reader >> is an example of "reflection" -- instead of using the |
251 | name of the method, we're using a variable with the name of the method in it. |
252 | Perl doesn't mind. Another way to write this would be |
253 | C<< $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 |
259 | And wrap up the example with a script to show off our newfound magic. |
8323a774 |
260 | |
261 | =head1 CONCLUSION |
262 | |
263 | Why oh why would you want to go through all of these contortions when you can |
264 | just 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 |
266 | probably also have it in the C<as_form> method, and C<to_file>, and so on. So |
267 | why not have a method that maps attribute names to labels? That could work, but |
268 | why not include the label where it belongs, in the attribute's definition? |
269 | That way you're also less likely to forget to add the label. |
270 | |
271 | More importantly, this was a very simple example. Your metaclasses aren't |
272 | limited to just adding new meta-attributes. For example, you could implement |
273 | a 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 | |
282 | The sky's the limit! |
283 | |
284 | =head1 AUTHOR |
285 | |
286 | Shawn M Moore E<lt>sartak@gmail.comE<gt> |
287 | |
288 | =head1 COPYRIGHT AND LICENSE |
289 | |
290 | Copyright 2006, 2007 by Infinity Interactive, Inc. |
291 | |
292 | L<http://www.iinteractive.com> |
293 | |
294 | This library is free software; you can redistribute it and/or modify |
295 | it under the same terms as Perl itself. |
296 | |
297 | =cut |
298 | |
299 | 1; |
300 | |