more tests, a missing file, bug fix
[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,
0a369903 126 default => sub{ shift->_build_plugin_app_ns },
127 trigger => sub{ $_[0]->_clear_plugin_locator
128 if $_[0]->_has_plugin_locator; },
129 );
208f7b27 130has _plugin_locator => (is => 'rw', required => 1, lazy => 1,
0a369903 131 isa => 'Module::Pluggable::Object',
132 clearer => '_clear_plugin_locator',
133 predicate => '_has_plugin_locator',
134 default => sub{ shift->_build_plugin_locator });
421e9f8d 135
421e9f8d 136#--------#---------#---------#---------#---------#---------#---------#---------#
137
138=head1 Public Methods
139
140=head2 load_plugin $plugin
141
9bef9c02 142This is the only method you should be using. Load the apropriate role for
143C<$plugin> as well as any extensions it provides if extensions are enabled.
421e9f8d 144
145=cut
146
147sub load_plugin{
148 my ($self, $plugin) = @_;
149 die("You must provide a plugin name") unless $plugin;
150
151 my $loaded = $self->_plugin_loaded;
152 return 1 if exists $loaded->{$plugin};
208f7b27 153
421e9f8d 154 my $role = $self->_role_from_plugin($plugin);
155
156 $loaded->{$plugin} = $role if $self->_load_and_apply_role($role);
157 $self->load_plugin_ext($plugin) if $self->_plugin_ext;
158
159 return exists $loaded->{$plugin};
160}
161
162
12d4facb 163=head2 load_plugin_ext
421e9f8d 164
165Will load any extensions for a particular plugin. This should be called
166automatically by C<load_plugin> so you don't need to worry about it.
167It basically attempts to load any extension that exists for a plugin
168that is already loaded. The only reason for using this is if you want to
169keep _plugin_ext as false and only load extensions manually, which I don't
170recommend.
171
172=cut
173
174sub load_plugin_ext{
175 my ($self, $plugin) = @_;
176 die("You must provide a plugin name") unless $plugin;
208f7b27 177 my $role = $self->_plugin_loaded->{$plugin};
421e9f8d 178
179 # $p for plugin, $r for role
180 while( my($p,$r) = each %{ $self->_plugin_loaded }){
421e9f8d 181
208f7b27 182 my $ext = join "::", $role, $self->_plugin_ext_ns, $p;
183 if( $plugin =~ /^\+(.*)/ ){
184 eval{ $self->_load_and_apply_role( $ext ) };
185 } else{
186 $self->_load_and_apply_role( $ext ) if
187 grep{ /^${ext}$/ } $self->_plugin_locator->plugins;
188 }
189
421e9f8d 190 #go back to prev loaded modules and load extensions for current module?
191 #my $ext2 = join "::", $r, $self->_plugin_ext_ns, $plugin;
192 #$self->_load_and_apply_role( $ext2 )
193 # if Class::Inspector->installed($ext2);
194 }
195}
196
9bef9c02 197=head2 _original_class_name
421e9f8d 198
9bef9c02 199Because of the way roles apply C<$self-E<gt>blessed> and C<ref $self> will
200no longer return what you expect. Instead use this class to get your original
201class name.
421e9f8d 202
203=cut
204
9bef9c02 205sub _original_class_name{
421e9f8d 206 my $self = shift;
9bef9c02 207 return (grep {$_ !~ /^Moose::/} $self->meta->class_precedence_list)[0];
421e9f8d 208}
209
9bef9c02 210
211=head1 Private Methods
212
213There's nothing stopping you from using these, but if you are using them
214for anything thats not really complicated you are probably doing
215something wrong. Some of these may be inlined in the future if performance
216becomes an issue (which I doubt).
217
421e9f8d 218=head2 _role_from_plugin $plugin
219
220Creates a role name from a plugin name. If the plugin name is prepended
221with a C<+> it will be treated as a full name returned as is. Otherwise
208f7b27 222a string consisting of C<$plugin> prepended with the C<_plugin_ns>
223and the first valid value from C<_plugin_app_ns> will be returned. Example
421e9f8d 224
225 #assuming appname MyApp and C<_plugin_ns> 'Plugin'
226 $self->_role_from_plugin("MyPlugin"); # MyApp::Plugin::MyPlugin
227
228=cut
229
230sub _role_from_plugin{
231 my ($self, $plugin) = @_;
232
208f7b27 233 return $1 if( $plugin =~ /^\+(.*)/ );
234
235 my $o = join '::', $self->_plugin_ns, $plugin;
236 #Father, please forgive me for I have sinned.
237 my @roles = grep{ /${o}$/ } $self->_plugin_locator->plugins;
238
239 die("Unable to locate plugin") unless @roles;
240 return $roles[0] if @roles == 1;
241
208f7b27 242 my $i = 0;
0a369903 243 my %presedence_list = map{ $i++; "${_}::${o}", $i } $self->_plugin_app_ns;
208f7b27 244
245 @roles = sort{ $presedence_list{$a} <=> $presedence_list{$b}} @roles;
421e9f8d 246
208f7b27 247 return shift @roles;
421e9f8d 248}
249
250=head2 _load_and_apply_role $role
251
9bef9c02 252Require C<$role> if it is not already loaded and apply it. This is
253the meat of this module.
421e9f8d 254
255=cut
256
257sub _load_and_apply_role{
258 my ($self, $role) = @_;
259 die("You must provide a role name") unless $role;
260
421e9f8d 261 #don't re-require...
208f7b27 262 unless( Class::MOP::is_class_loaded($role) ){
263 eval Class::MOP::load_class($role) || die("Failed to load role: $role");
421e9f8d 264 }
265
266 carp("Using 'override' is strongly discouraged and may not behave ".
267 "as you expect it to. Please use 'around'")
268 if scalar keys %{ $role->meta->get_override_method_modifiers_map };
269
270 #apply the plugin to the anon subclass
271 die("Failed to apply plugin: $role")
9bef9c02 272 unless $role->meta->apply( $self );
421e9f8d 273
274 return 1;
275}
276
208f7b27 277=head2 _build_plugin_app_ns
278
279Automatically builds the _plugin_app_ns attribute with the classes in the
280class presedence list that are not part of Moose.
281
282=cut
283
284sub _build_plugin_app_ns{
285 my $self = shift;
286 my @names = (grep {$_ !~ /^Moose::/} $self->meta->class_precedence_list);
287 return \@names;
288}
289
290=head2 _build_plugin_locator
291
292Automatically creates a L<Module::Pluggable::Object> instance with the correct
293search_path.
294
295=cut
296
297sub _build_plugin_locator{
298 my $self = shift;
299
300 my $locator = Module::Pluggable::Object->new
301 ( search_path =>
302 [ map { join '::', ($_, $self->_plugin_ns) } $self->_plugin_app_ns ]
303 );
304 return $locator;
305}
306
12d4facb 307=head2 meta
308
309Keep tests happy. See L<Moose>
310
311=cut
312
421e9f8d 3131;
314
315__END__;
316
317=head1 SEE ALSO
318
9bef9c02 319L<Moose>, L<Moose::Role>, L<Class::Inspector>
421e9f8d 320
321=head1 AUTHOR
322
323Guillermo Roditi, <groditi@cpan.org>
324
325=head1 BUGS
326
327Holler?
328
329Please report any bugs or feature requests to
330C<bug-moosex-object-pluggable at rt.cpan.org>, or through the web interface at
331L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MooseX-Object-Pluggable>.
332I will be notified, and then you'll automatically be notified of progress on
333your bug as I make changes.
334
335=head1 SUPPORT
336
337You can find documentation for this module with the perldoc command.
338
339 perldoc MooseX-Object-Pluggable
340
341You can also look for information at:
342
343=over 4
344
345=item * AnnoCPAN: Annotated CPAN documentation
346
347L<http://annocpan.org/dist/MooseX-Object-Pluggable>
348
349=item * CPAN Ratings
350
351L<http://cpanratings.perl.org/d/MooseX-Object-Pluggable>
352
353=item * RT: CPAN's request tracker
354
355L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=MooseX-Object-Pluggable>
356
357=item * Search CPAN
358
359L<http://search.cpan.org/dist/MooseX-Object-Pluggable>
360
361=back
362
363=head1 ACKNOWLEDGEMENTS
364
365=over 4
366
367=item #Moose - Huge number of questions
368
369=item Matt S Trout <mst@shadowcatsystems.co.uk> - ideas / planning.
370
371=item Stevan Little - EVERYTHING. Without him this would have never happened.
372
373=back
374
375=head1 COPYRIGHT
376
377Copyright 2007 Guillermo Roditi. All Rights Reserved. This is
378free software; you may redistribute it and/or modify it under the same
379terms as Perl itself.
380
381=cut