Commit | Line | Data |
8a788638 |
1 | package Moose::Cookbook::Meta::PrivateOrPublic_MethodMetaclass; |
b1d7ad19 |
2 | |
daa0fd7d |
3 | # ABSTRACT: A method metaclass for marking methods public or private |
4 | |
5 | __END__ |
b1d7ad19 |
6 | |
b1d7ad19 |
7 | |
daa0fd7d |
8 | =pod |
b1d7ad19 |
9 | |
10 | =head1 SYNOPSIS |
11 | |
909c3b96 |
12 | package MyApp::Meta::Method::PrivateOrPublic; |
b1d7ad19 |
13 | |
14 | use Moose; |
15 | use Moose::Util::TypeConstraints; |
16 | |
17 | extends 'Moose::Meta::Method'; |
18 | |
19 | has '_policy' => ( |
20 | is => 'ro', |
21 | isa => enum( [ qw( public private ) ] ), |
22 | default => 'public', |
23 | init_arg => 'policy', |
24 | ); |
25 | |
26 | sub new { |
27 | my $class = shift; |
28 | my %options = @_; |
29 | |
30 | my $self = $class->SUPER::wrap(%options); |
31 | |
32 | $self->{_policy} = $options{policy}; |
33 | |
34 | $self->_add_policy_wrapper; |
35 | |
36 | return $self; |
37 | } |
38 | |
39 | sub _add_policy_wrapper { |
40 | my $self = shift; |
41 | |
42 | return if $self->is_public; |
43 | |
44 | my $name = $self->name; |
45 | my $package = $self->package_name; |
46 | my $real_body = $self->body; |
47 | |
48 | my $body = sub { |
49 | die "The $package\::$name method is private" |
50 | unless ( scalar caller() ) eq $package; |
51 | |
52 | goto &{$real_body}; |
53 | }; |
54 | |
55 | $self->{body} = $body; |
56 | } |
57 | |
58 | sub is_public { $_[0]->_policy eq 'public' } |
59 | sub is_private { $_[0]->_policy eq 'private' } |
60 | |
61 | package MyApp::User; |
62 | |
63 | use Moose; |
64 | |
65 | has 'password' => ( is => 'rw' ); |
66 | |
67 | __PACKAGE__->meta()->add_method( |
68 | '_reset_password', |
909c3b96 |
69 | MyApp::Meta::Method::PrivateOrPublic->new( |
b1d7ad19 |
70 | name => '_reset_password', |
71 | package_name => __PACKAGE__, |
72 | body => sub { $_[0]->password('reset') }, |
73 | policy => 'private', |
74 | ) |
75 | ); |
76 | |
77 | =head1 DESCRIPTION |
78 | |
79 | This example shows a custom method metaclass that models public versus |
80 | private methods. If a method is defined as private, it adds a wrapper |
81 | around the method which dies unless it is called from the class where |
82 | it was defined. |
83 | |
84 | The way the method is added to the class is rather ugly. If we wanted |
85 | to make this a real feature, we'd probably want to add some sort of |
86 | sugar to allow us to declare private methods, but that is beyond the |
87 | scope of this recipe. See the Extending recipes for more on this |
88 | topic. |
89 | |
90 | The core of our custom class is the C<policy> attribute, and |
91 | C<_add_policy_wrapper> method. |
92 | |
93 | You'll note that we have to explicitly set the C<policy> attribute in |
94 | our constructor: |
95 | |
22a6230f |
96 | $self->{_policy} = $options{policy}; |
b1d7ad19 |
97 | |
98 | That is necessary because Moose metaclasses do not use the meta API to |
66d68f76 |
99 | create objects. Most Moose classes have a custom "inlined" constructor |
b1d7ad19 |
100 | for speed. |
101 | |
102 | In this particular case, our parent class's constructor is the C<wrap> |
103 | method. We call that to build our object, but it does not include |
104 | subclass-specific attributes. |
105 | |
106 | The C<_add_policy_wrapper> method is where the real work is done. If |
107 | the method is private, we construct a wrapper around the real |
108 | subroutine which checks that the caller matches the package in which |
109 | the subroutine was created. |
110 | |
111 | If they don't match, it dies. If they do match, the real method is |
112 | called. We use C<goto> so that the wrapper does not show up in the |
113 | call stack. |
114 | |
115 | Finally, we replace the value of C<< $self->{body} >>. This is another |
116 | case where we have to do something a bit gross because Moose does not |
117 | use Moose for its own implementation. |
118 | |
119 | When we pass this method object to the metaclass's C<add_method> |
120 | method, it will take the method body and make it available in the |
121 | class. |
122 | |
123 | Finally, when we retrieve these methods via the introspection API, we |
124 | can call the C<is_public> and C<is_private> methods on them to get |
125 | more information about the method. |
126 | |
127 | =head1 SUMMARY |
128 | |
129 | A custom method metaclass lets us add both behavior and |
130 | meta-information to methods. Unfortunately, because the Perl |
2a67e654 |
131 | interpreter does not provide easy hooks into method declaration, the |
b1d7ad19 |
132 | API we have for adding these methods is not very pretty. |
133 | |
134 | That can be improved with custom Moose-like sugar, or even by using a |
135 | tool like L<Devel::Declare> to create full-blown new keywords in Perl. |
136 | |
b1d7ad19 |
137 | =begin testing |
138 | |
139 | package main; |
140 | |
7c745cd9 |
141 | use Test::Fatal; |
b1d7ad19 |
142 | |
143 | my $user = MyApp::User->new( password => 'foo!' ); |
144 | |
7c745cd9 |
145 | like( exception { $user->_reset_password }, |
b1d7ad19 |
146 | qr/The MyApp::User::_reset_password method is private/, |
7c745cd9 |
147 | '_reset_password method dies if called outside MyApp::User class'); |
b1d7ad19 |
148 | |
149 | { |
150 | package MyApp::User; |
151 | |
152 | sub run_reset { $_[0]->_reset_password } |
153 | } |
154 | |
155 | $user->run_reset; |
156 | |
157 | is( $user->password, 'reset', 'password has been reset' ); |
158 | |
159 | =end testing |
160 | |
161 | =cut |