irc is bad for productivity
[gitmo/MooseX-Object-Pluggable.git] / lib / MooseX / Object / Pluggable.pm
CommitLineData
421e9f8d 1package MooseX::Object::Pluggable;
2
3use Carp;
4use strict;
5use warnings;
6use Moose::Role;
208f7b27 7use Class::MOP;
8use Module::Pluggable::Object;
421e9f8d 9
208f7b27 10our $VERSION = '0.0005';
421e9f8d 11
12=head1 NAME
13
14 MooseX::Object::Pluggable - Make your classes pluggable
15
16=head1 SYNOPSIS
17
18 package MyApp;
19 use Moose;
20
21 with 'MooseX::Object::Pluggable';
22
23 ...
24
25 package MyApp::Plugin::Pretty;
26 use Moose::Role;
27
28 sub pretty{ print "I am pretty" }
29
30 1;
31
32 #
33 use MyApp;
34 my $app = MyApp->new;
35 $app->load_plugin('Pretty');
36 $app->pretty;
37
38=head1 DESCRIPTION
39
40This module is meant to be loaded as a role from Moose-based classes
9bef9c02 41it will add five methods and four attributes to assist you with the loading
421e9f8d 42and handling of plugins and extensions for plugins. I understand that this may
43pollute your namespace, however I took great care in using the least ambiguous
44names possible.
45
46=head1 How plugins Work
47
48Plugins and extensions are just Roles by a fancy name. They are loaded at runtime
49on demand and are instance, not class based. This means that if you have more than
50one instance of a class they can all have different plugins loaded. This is a feature.
51
52Plugin methods are allowed to C<around>, C<before>, C<after>
53their consuming classes, so it is important to watch for load order as plugins can
54and will overload each other. You may also add attributes through has.
55
06c9b714 56Please note that when you load at runtime you lose the ability to wrap C<BUILD>
9bef9c02 57and roles using C<has> will not go through comile time checks like C<required>
58and <default>.
59
06c9b714 60Even though C<override> will work , I STRONGLY discourage it's use
421e9f8d 61and a warning will be thrown if you try to use it.
62This is closely linked to the way multiple roles being applies is handles and is not
63likely to change. C<override> bevavior is closely linked to inheritance and thus will
64likely not work as you expect it in multiple inheritance situations. Point being,
65save yourself the headache.
66
67=head1 How plugins are loaded
68
9bef9c02 69When roles are applied at runtime an anonymous class will wrap your class and
421e9f8d 70C<$self-E<gt>blessed> and C<ref $self> will no longer return the name of your object,
9bef9c02 71they will instead return the name of the anonymous class created at runtime.
72See C<_original_class_name>.
421e9f8d 73
9bef9c02 74=head1 Usage
421e9f8d 75
9bef9c02 76For a simple example see the tests included in this distribution.
421e9f8d 77
78=head1 Attributes
79
80=head2 _plugin_ns
81
82String. The prefix to use for plugin names provided. MyApp::Plugin is sensible.
83
84=head2 _plugin_ext
85
86Boolean. Indicates whether we should attempt to load plugin extensions.
87Defaults to true;
88
89=head2 _plugin_ext_ns
90
91String. The namespace plugin extensions have. Defaults to 'ExtensionFor'.
92
93This means that is _plugin_ns is "MyApp::Plugin" and _plugin_ext_ns is
94"ExtensionFor" loading plugin "Bar" would search for extensions in
95"MyApp::Plugin::Bar::ExtensionFor::*".
96
208f7b27 97=head2 _plugin_app_ns
421e9f8d 98
208f7b27 99ArrayRef, Accessor automatically dereferences into array on a read call.
100By default will be filled with the class name and it's prescedents, it is used
101to determine which directories to look for plugins as well as which plugins
102take presedence upon namespace collitions. This allows you to subclass a pluggable
103class and still use it's plugins while using yours first if they are available.
421e9f8d 104
421e9f8d 105=cut
106
208f7b27 107=head2 _plugin_locator
108
109An automatically built instance of L<Module::Pluggable::Object> used to locate
110available plugins.
111
112=cut
421e9f8d 113
208f7b27 114#--------#---------#---------#---------#---------#---------#---------#---------#
421e9f8d 115
208f7b27 116has _plugin_ns => (is => 'rw', required => 1, isa => 'Str',
117 default => 'Plugin');
118has _plugin_ext => (is => 'rw', required => 1, isa => 'Bool',
119 default => 1);
120has _plugin_ext_ns => (is => 'rw', required => 1, isa => 'Str',
121 default => 'ExtensionFor');
122has _plugin_loaded => (is => 'rw', required => 1, isa => 'HashRef',
123 default => sub{ {} });
124has _plugin_app_ns => (is => 'rw', required => 1, isa => 'ArrayRef', lazy => 1,
125 auto_deref => 1,
126 default => sub{ shift->_build_plugin_app_ns });
127has _plugin_locator => (is => 'rw', required => 1, lazy => 1,
128 isa => 'Module::Pluggable::Object',
129 default => sub{ shift->_build_plugin_locator });
421e9f8d 130
421e9f8d 131#--------#---------#---------#---------#---------#---------#---------#---------#
132
133=head1 Public Methods
134
135=head2 load_plugin $plugin
136
9bef9c02 137This is the only method you should be using. Load the apropriate role for
138C<$plugin> as well as any extensions it provides if extensions are enabled.
421e9f8d 139
140=cut
141
142sub load_plugin{
143 my ($self, $plugin) = @_;
144 die("You must provide a plugin name") unless $plugin;
145
146 my $loaded = $self->_plugin_loaded;
147 return 1 if exists $loaded->{$plugin};
208f7b27 148
421e9f8d 149 my $role = $self->_role_from_plugin($plugin);
150
151 $loaded->{$plugin} = $role if $self->_load_and_apply_role($role);
152 $self->load_plugin_ext($plugin) if $self->_plugin_ext;
153
154 return exists $loaded->{$plugin};
155}
156
157
12d4facb 158=head2 load_plugin_ext
421e9f8d 159
160Will load any extensions for a particular plugin. This should be called
161automatically by C<load_plugin> so you don't need to worry about it.
162It basically attempts to load any extension that exists for a plugin
163that is already loaded. The only reason for using this is if you want to
164keep _plugin_ext as false and only load extensions manually, which I don't
165recommend.
166
167=cut
168
169sub load_plugin_ext{
170 my ($self, $plugin) = @_;
171 die("You must provide a plugin name") unless $plugin;
208f7b27 172 my $role = $self->_plugin_loaded->{$plugin};
421e9f8d 173
174 # $p for plugin, $r for role
175 while( my($p,$r) = each %{ $self->_plugin_loaded }){
421e9f8d 176
208f7b27 177 my $ext = join "::", $role, $self->_plugin_ext_ns, $p;
178 if( $plugin =~ /^\+(.*)/ ){
179 eval{ $self->_load_and_apply_role( $ext ) };
180 } else{
181 $self->_load_and_apply_role( $ext ) if
182 grep{ /^${ext}$/ } $self->_plugin_locator->plugins;
183 }
184
421e9f8d 185 #go back to prev loaded modules and load extensions for current module?
186 #my $ext2 = join "::", $r, $self->_plugin_ext_ns, $plugin;
187 #$self->_load_and_apply_role( $ext2 )
188 # if Class::Inspector->installed($ext2);
189 }
190}
191
9bef9c02 192=head2 _original_class_name
421e9f8d 193
9bef9c02 194Because of the way roles apply C<$self-E<gt>blessed> and C<ref $self> will
195no longer return what you expect. Instead use this class to get your original
196class name.
421e9f8d 197
198=cut
199
9bef9c02 200sub _original_class_name{
421e9f8d 201 my $self = shift;
9bef9c02 202 return (grep {$_ !~ /^Moose::/} $self->meta->class_precedence_list)[0];
421e9f8d 203}
204
9bef9c02 205
206=head1 Private Methods
207
208There's nothing stopping you from using these, but if you are using them
209for anything thats not really complicated you are probably doing
210something wrong. Some of these may be inlined in the future if performance
211becomes an issue (which I doubt).
212
421e9f8d 213=head2 _role_from_plugin $plugin
214
215Creates a role name from a plugin name. If the plugin name is prepended
216with a C<+> it will be treated as a full name returned as is. Otherwise
208f7b27 217a string consisting of C<$plugin> prepended with the C<_plugin_ns>
218and the first valid value from C<_plugin_app_ns> will be returned. Example
421e9f8d 219
220 #assuming appname MyApp and C<_plugin_ns> 'Plugin'
221 $self->_role_from_plugin("MyPlugin"); # MyApp::Plugin::MyPlugin
222
223=cut
224
225sub _role_from_plugin{
226 my ($self, $plugin) = @_;
227
208f7b27 228 return $1 if( $plugin =~ /^\+(.*)/ );
229
230 my $o = join '::', $self->_plugin_ns, $plugin;
231 #Father, please forgive me for I have sinned.
232 my @roles = grep{ /${o}$/ } $self->_plugin_locator->plugins;
233
234 die("Unable to locate plugin") unless @roles;
235 return $roles[0] if @roles == 1;
236
237 my @names = (grep {$_ !~ /^Moose::/} $self->meta->class_precedence_list);
238 my $i = 0;
239 my %presedence_list = map{ $i++; "${_}::${o}", $i } @names;
240
241 @roles = sort{ $presedence_list{$a} <=> $presedence_list{$b}} @roles;
421e9f8d 242
208f7b27 243 return shift @roles;
421e9f8d 244}
245
246=head2 _load_and_apply_role $role
247
9bef9c02 248Require C<$role> if it is not already loaded and apply it. This is
249the meat of this module.
421e9f8d 250
251=cut
252
253sub _load_and_apply_role{
254 my ($self, $role) = @_;
255 die("You must provide a role name") unless $role;
256
421e9f8d 257 #don't re-require...
208f7b27 258 unless( Class::MOP::is_class_loaded($role) ){
259 eval Class::MOP::load_class($role) || die("Failed to load role: $role");
421e9f8d 260 }
261
262 carp("Using 'override' is strongly discouraged and may not behave ".
263 "as you expect it to. Please use 'around'")
264 if scalar keys %{ $role->meta->get_override_method_modifiers_map };
265
266 #apply the plugin to the anon subclass
267 die("Failed to apply plugin: $role")
9bef9c02 268 unless $role->meta->apply( $self );
421e9f8d 269
270 return 1;
271}
272
208f7b27 273=head2 _build_plugin_app_ns
274
275Automatically builds the _plugin_app_ns attribute with the classes in the
276class presedence list that are not part of Moose.
277
278=cut
279
280sub _build_plugin_app_ns{
281 my $self = shift;
282 my @names = (grep {$_ !~ /^Moose::/} $self->meta->class_precedence_list);
283 return \@names;
284}
285
286=head2 _build_plugin_locator
287
288Automatically creates a L<Module::Pluggable::Object> instance with the correct
289search_path.
290
291=cut
292
293sub _build_plugin_locator{
294 my $self = shift;
295
296 my $locator = Module::Pluggable::Object->new
297 ( search_path =>
298 [ map { join '::', ($_, $self->_plugin_ns) } $self->_plugin_app_ns ]
299 );
300 return $locator;
301}
302
12d4facb 303=head2 meta
304
305Keep tests happy. See L<Moose>
306
307=cut
308
421e9f8d 3091;
310
311__END__;
312
313=head1 SEE ALSO
314
9bef9c02 315L<Moose>, L<Moose::Role>, L<Class::Inspector>
421e9f8d 316
317=head1 AUTHOR
318
319Guillermo Roditi, <groditi@cpan.org>
320
321=head1 BUGS
322
323Holler?
324
325Please report any bugs or feature requests to
326C<bug-moosex-object-pluggable at rt.cpan.org>, or through the web interface at
327L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MooseX-Object-Pluggable>.
328I will be notified, and then you'll automatically be notified of progress on
329your bug as I make changes.
330
331=head1 SUPPORT
332
333You can find documentation for this module with the perldoc command.
334
335 perldoc MooseX-Object-Pluggable
336
337You can also look for information at:
338
339=over 4
340
341=item * AnnoCPAN: Annotated CPAN documentation
342
343L<http://annocpan.org/dist/MooseX-Object-Pluggable>
344
345=item * CPAN Ratings
346
347L<http://cpanratings.perl.org/d/MooseX-Object-Pluggable>
348
349=item * RT: CPAN's request tracker
350
351L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=MooseX-Object-Pluggable>
352
353=item * Search CPAN
354
355L<http://search.cpan.org/dist/MooseX-Object-Pluggable>
356
357=back
358
359=head1 ACKNOWLEDGEMENTS
360
361=over 4
362
363=item #Moose - Huge number of questions
364
365=item Matt S Trout <mst@shadowcatsystems.co.uk> - ideas / planning.
366
367=item Stevan Little - EVERYTHING. Without him this would have never happened.
368
369=back
370
371=head1 COPYRIGHT
372
373Copyright 2007 Guillermo Roditi. All Rights Reserved. This is
374free software; you may redistribute it and/or modify it under the same
375terms as Perl itself.
376
377=cut