Commit | Line | Data |
b1301316 |
1 | package Moose::Cookbook::Meta::Labeled_AttributeTrait; |
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 | |
fe66eda1 |
68 | =head1 SUMMARY |
aff0421c |
69 | |
fe66eda1 |
70 | In this recipe, we begin to delve into the wonder of meta-programming. |
71 | Some readers may scoff and claim that this is the arena of only the |
72 | most twisted Moose developers. Absolutely not! Any sufficiently |
73 | twisted developer can benefit greatly from going more meta. |
aff0421c |
74 | |
fe66eda1 |
75 | Our goal is to allow each attribute to have a human-readable "label" |
76 | attached to it. Such labels would be used when showing data to an end |
77 | user. In this recipe we label the C<url> attribute with "The site's |
78 | URL" and create a simple method showing how to use that label. |
aff0421c |
79 | |
fe66eda1 |
80 | =head1 META-ATTRIBUTE OBJECTS |
4515e88e |
81 | |
fe66eda1 |
82 | All the attributes of a Moose-based object are actually objects themselves. |
83 | These objects have methods and attributes. Let's look at a concrete example. |
4515e88e |
84 | |
fe66eda1 |
85 | has 'x' => ( isa => 'Int', is => 'ro' ); |
86 | has 'y' => ( isa => 'Int', is => 'rw' ); |
87 | |
88 | Internally, the metaclass for C<Point> has two L<Moose::Meta::Attribute> |
89 | objects. There are several methods for getting meta-attributes out of a |
90 | metaclass, one of which is C<get_attribute_list>. This method is called on the |
91 | metaclass object. |
92 | |
93 | The C<get_attribute_list> method returns a list of attribute names. You can |
94 | then use C<get_attribute> to get the L<Moose::Meta::Attribute> object itself. |
95 | |
96 | Once you have this meta-attribute object, you can call methods on it like |
97 | this: |
98 | |
99 | print $point->meta->get_attribute('x')->type_constraint; |
100 | => Int |
101 | |
102 | To add a label to our attributes there are two steps. First, we need a new |
103 | attribute metaclass trait that can store a label for an attribute. Second, we |
104 | need to apply that trait to our attributes. |
aff0421c |
105 | |
106 | =head1 TRAITS |
107 | |
4515e88e |
108 | Roles that apply to metaclasses have a special name: traits. Don't let |
109 | the change in nomenclature fool you, B<traits are just roles>. |
aff0421c |
110 | |
4515e88e |
111 | L<Moose/has> allows you to pass a C<traits> parameter for an |
112 | attribute. This parameter takes a list of trait names which are |
113 | composed into an anonymous metaclass, and that anonymous metaclass is |
114 | used for the attribute. |
aff0421c |
115 | |
4515e88e |
116 | Yes, we still have lots of metaclasses in the background, but they're |
117 | managed by Moose for you. |
118 | |
119 | Traits can do anything roles can do. They can add or refine |
120 | attributes, wrap methods, provide more methods, define an interface, |
121 | etc. The only difference is that you're now changing the attribute |
122 | metaclass instead of a user-level class. |
aff0421c |
123 | |
124 | =head1 DISSECTION |
125 | |
fe66eda1 |
126 | We start by creating a package for our trait. |
aff0421c |
127 | |
6a7e3999 |
128 | package MyApp::Meta::Attribute::Trait::Labeled; |
129 | use Moose::Role; |
aff0421c |
130 | |
6a7e3999 |
131 | has label => ( |
132 | is => 'rw', |
133 | isa => 'Str', |
134 | predicate => 'has_label', |
135 | ); |
aff0421c |
136 | |
fe66eda1 |
137 | You can see that a trait is just a L<Moose::Role>. In this case, our role |
138 | contains a single attribute, C<label>. Any attribute which does this trait |
139 | will now have a label. |
140 | |
141 | Next we register our trait with Moose: |
aff0421c |
142 | |
6a7e3999 |
143 | package Moose::Meta::Attribute::Custom::Trait::Labeled; |
144 | sub register_implementation { 'MyApp::Meta::Attribute::Trait::Labeled' } |
aff0421c |
145 | |
4515e88e |
146 | Moose looks for the C<register_implementation> method in |
aff0421c |
147 | C<Moose::Meta::Attribute::Custom::Trait::$TRAIT_NAME> to find the full |
148 | name of the trait. |
149 | |
fe66eda1 |
150 | Finally, we pass our trait when defining an attribute: |
aff0421c |
151 | |
6a7e3999 |
152 | has url => ( |
153 | traits => [qw/Labeled/], |
154 | is => 'rw', |
155 | isa => 'Str', |
156 | label => "The site's URL", |
157 | ); |
aff0421c |
158 | |
fe66eda1 |
159 | The C<traits> parameter contains a list of trait names. Moose will build an |
4515e88e |
160 | anonymous attribute metaclass from these traits and use it for this |
fe66eda1 |
161 | attribute. |
162 | |
163 | The reason that we can pass the name C<Labeled>, instead of |
164 | C<MyApp::Meta::Attribute::Trait::Labeled>, is because of the |
165 | C<register_implementation> code we touched on previously. |
166 | |
167 | When you pass a metaclass to C<has>, it will take the name you provide and |
168 | prefix it with C<Moose::Meta::Attribute::Custom::Trait::>. Then it calls |
169 | C<register_implementation> in the package. In this case, that means Moose ends |
170 | up calling |
171 | C<Moose::Meta::Attribute::Custom::Trait::Labeled::register_implementation>. |
172 | |
173 | If this function exists, it should return the I<real> trait's package |
174 | name. This is exactly what our code does, returning |
175 | C<MyApp::Meta::Attribute::Trait::Labeled>. This is a little convoluted, and if |
176 | you don't like it, you can always use the fully-qualified name. |
177 | |
178 | We can access this meta-attribute and its label like this: |
179 | |
180 | $website->meta->get_attribute('url')->label() |
181 | |
182 | MyApp::Website->meta->get_attribute('url')->label() |
183 | |
184 | We also have a regular attribute, C<name>: |
185 | |
186 | has name => ( |
187 | is => 'rw', |
188 | isa => 'Str', |
189 | ); |
190 | |
191 | Finally, we have a C<dump> method, which creates a human-readable |
192 | representation of a C<MyApp::Website> object. It will use an attribute's label |
193 | if it has one. |
194 | |
195 | sub dump { |
196 | my $self = shift; |
197 | |
198 | my $meta = $self->meta; |
199 | |
200 | my $dump = ''; |
201 | |
202 | for my $attribute ( map { $meta->get_attribute($_) } |
203 | sort $meta->get_attribute_list ) { |
aff0421c |
204 | |
c79239a2 |
205 | if ( $attribute->does('MyApp::Meta::Attribute::Trait::Labeled') |
206 | && $attribute->has_label ) { |
207 | $dump .= $attribute->label; |
208 | } |
aff0421c |
209 | |
fe66eda1 |
210 | This is a bit of defensive code. We cannot depend on every meta-attribute |
211 | having a label. Even if we define one for every attribute in our class, a |
212 | subclass may neglect to do so. Or a superclass could add an attribute without |
213 | a label. |
aff0421c |
214 | |
fe66eda1 |
215 | We also check that the attribute has a label using the predicate we |
216 | defined. We could instead make the label C<required>. If we have a label, we |
217 | use it, otherwise we use the attribute name: |
aff0421c |
218 | |
fe66eda1 |
219 | else { |
220 | $dump .= $attribute->name; |
221 | } |
d9a8643f |
222 | |
fe66eda1 |
223 | my $reader = $attribute->get_read_method; |
224 | $dump .= ": " . $self->$reader . "\n"; |
225 | } |
d9a8643f |
226 | |
fe66eda1 |
227 | return $dump; |
228 | } |
d9a8643f |
229 | |
fe66eda1 |
230 | The C<get_read_method> is part of the L<Moose::Meta::Attribute> API. It |
231 | returns the name of a method that can read the attribute's value, I<when |
232 | called on the real object> (don't call this on the meta-attribute). |
d9a8643f |
233 | |
fe66eda1 |
234 | =head1 CONCLUSION |
d9a8643f |
235 | |
fe66eda1 |
236 | You might wonder why you'd bother with all this. You could just hardcode "The |
237 | Site's URL" in the C<dump> method. But we want to avoid repetition. If you |
238 | need the label once, you may need it elsewhere, maybe in the C<as_form> method |
239 | you write next. |
d9a8643f |
240 | |
fe66eda1 |
241 | Associating a label with an attribute just makes sense! The label is a piece |
242 | of information I<about> the attribute. |
243 | |
244 | It's also important to realize that this was a trivial example. You can make |
245 | much more powerful metaclasses that I<do> things, as opposed to just storing |
246 | some more information. For example, you could implement a metaclass that |
247 | expires attributes after a certain amount of time: |
aff0421c |
248 | |
fe66eda1 |
249 | has site_cache => ( |
250 | traits => ['TimedExpiry'], |
251 | expires_after => { hours => 1 }, |
252 | refresh_with => sub { get( $_[0]->url ) }, |
253 | isa => 'Str', |
254 | is => 'ro', |
255 | ); |
256 | |
257 | The sky's the limit! |
aff0421c |
258 | |
c79239a2 |
259 | =begin testing |
aff0421c |
260 | |
fe66eda1 |
261 | my $app |
262 | = MyApp::Website->new( url => 'http://google.com', name => 'Google' ); |
c79239a2 |
263 | is( |
fe66eda1 |
264 | $app->dump, q{name: Google |
c79239a2 |
265 | The site's URL: http://google.com |
266 | }, '... got the expected dump value' |
267 | ); |
aff0421c |
268 | |
c79239a2 |
269 | =end testing |
270 | |
271 | =cut |