Add Class::C3::Componetised::ApplyHooks features
[p5sagit/Class-C3-Componentised.git] / lib / Class / C3 / Componentised.pm
CommitLineData
d288ce53 1package Class::C3::Componentised;
2
20169807 3=head1 NAME
4
5Class::C3::Componentised
6
7=head1 DESCRIPTION
8
9Load mix-ins or components to your C3-based class.
10
11=head1 SYNOPSIS
12
13 package MyModule;
14
15 use strict;
16 use warnings;
17
18 use base 'Class::C3::Componentised';
19
0c205e9c 20 sub component_base_class { "MyModule::Component" }
20169807 21
22 package main;
23
0c205e9c 24 MyModule->load_components( qw/Foo Bar/ );
b34a4025 25 # Will load MyModule::Component::Foo and MyModule::Component::Bar
0c205e9c 26
27=head1 DESCRIPTION
28
29This will inject base classes to your module using the L<Class::C3> method
30resolution order.
31
32Please note: these are not plugins that can take precedence over methods
33declared in MyModule. If you want something like that, consider
34L<MooseX::Object::Pluggable>.
20169807 35
36=head1 METHODS
37
38=cut
39
d288ce53 40use strict;
41use warnings;
42
3a4635fb 43# This will prime the Class::C3 namespace (either by loading it proper on 5.8
44# or by installing compat shims on 5.10+). A user might have a reasonable
45# expectation that using Class::C3::<something> will give him access to
46# Class::C3 itself, and this module has been providing this historically.
47# Therefore leaving it in indefinitely.
0b8e135a 48use MRO::Compat;
3a4635fb 49
f8b4872f 50use Carp ();
d288ce53 51
459c3ae3 52our $VERSION = 1.0009;
d288ce53 53
eac9b176 54my $invalid_class = qr/(?: \b:\b | \:{3,} | \:\:$ )/x;
55
20169807 56=head2 load_components( @comps )
d288ce53 57
20169807 58Loads the given components into the current module. If a module begins with a
59C<+> character, it is taken to be a fully qualified class name, otherwise
60C<< $class->component_base_class >> is prepended to it.
d288ce53 61
20169807 62Calling this will call C<Class::C3::reinitialize>.
63
64=cut
d288ce53 65
66sub load_components {
67 my $class = shift;
3a4635fb 68 $class->_load_components( map {
69 /^\+(.*)$/
70 ? $1
71 : join ('::', $class->component_base_class, $_)
72 } grep { $_ !~ /^#/ } @_
73 );
d288ce53 74}
75
20169807 76=head2 load_own_components( @comps )
77
5e54b45d 78Similar to L<load_components>, but assumes every class is C<"$class::$comp">.
20169807 79
80=cut
81
d288ce53 82sub load_own_components {
83 my $class = shift;
3a4635fb 84 $class->_load_components( map { "${class}::$_" } grep { $_ !~ /^#/ } @_ );
d288ce53 85}
86
87sub _load_components {
3a4635fb 88 my $class = shift;
89 return unless @_;
90
91 $class->ensure_class_loaded($_) for @_;
92 $class->inject_base($class => @_);
20169807 93 Class::C3::reinitialize();
d288ce53 94}
95
20169807 96=head2 load_optional_components
d288ce53 97
20169807 98As L<load_components>, but will silently ignore any components that cannot be
99found.
d288ce53 100
20169807 101=cut
d288ce53 102
20169807 103sub load_optional_components {
104 my $class = shift;
3a4635fb 105 $class->_load_components( grep
106 { $class->load_optional_class( $_ ) }
107 ( map
108 { /^\+(.*)$/
109 ? $1
110 : join ('::', $class->component_base_class, $_)
111 }
112 grep { $_ !~ /^#/ } @_
113 )
114 );
20169807 115}
d288ce53 116
20169807 117=head2 ensure_class_loaded
118
119Given a class name, tests to see if it is already loaded or otherwise
120defined. If it is not yet loaded, the package is require'd, and an exception
121is thrown if the class is still not loaded.
122
123 BUG: For some reason, packages with syntax errors are added to %INC on
124 require
125=cut
126
20169807 127sub ensure_class_loaded {
128 my ($class, $f_class) = @_;
129
3a4635fb 130 no strict 'refs';
131
132 # ripped from Class::Inspector for speed
133 # note that the order is important (faster items are first)
134 return if ${"${f_class}::VERSION"};
135
136 return if @{"${f_class}::ISA"};
137
138 my $file = (join ('/', split ('::', $f_class) ) ) . '.pm';
139 return if $INC{$file};
140
141 for ( keys %{"${f_class}::"} ) {
142 return if ( *{"${f_class}::$_"}{CODE} );
143 }
144
3a4635fb 145 # require always returns true on success
15b7d164 146 # ill-behaved modules might very well obliterate $_
147 eval { local $_; require($file) } or do {
3a4635fb 148
eac9b176 149 $@ = "Invalid class name '$f_class'" if $f_class =~ $invalid_class;
3a4635fb 150
20169807 151 if ($class->can('throw_exception')) {
152 $class->throw_exception($@);
153 } else {
f8b4872f 154 Carp::croak $@;
20169807 155 }
3a4635fb 156 };
157
158 return;
20169807 159}
d288ce53 160
20169807 161=head2 ensure_class_found
d288ce53 162
20169807 163Returns true if the specified class is installed or already loaded, false
b34a4025 164otherwise.
165
166Note that the underlying mechanism (Class::Inspector->installed()) used by this
167sub will not, at the time of writing, correctly function when @INC includes
168coderefs. Since PAR relies upon coderefs in @INC, this function should be
169avoided in modules that are likely to be included within a PAR.
d288ce53 170
20169807 171=cut
d288ce53 172
20169807 173sub ensure_class_found {
3a4635fb 174 #my ($class, $f_class) = @_;
175 require Class::Inspector;
176 return Class::Inspector->loaded($_[1]) ||
177 Class::Inspector->installed($_[1]);
20169807 178}
d288ce53 179
d288ce53 180
181=head2 inject_base
182
20169807 183Does the actual magic of adjusting @ISA on the target module.
184
185=cut
d288ce53 186
20169807 187sub inject_base {
3a4635fb 188 my $class = shift;
189 my $target = shift;
190
e6b8b400 191 mro::set_mro($target, 'c3');
192
193 for my $comp (reverse @_) {
20169807 194 no strict 'refs';
e6b8b400 195 unless ($target eq $comp || $target->isa($comp)) {
196 my @heritage = @{mro::get_linear_isa($comp)};
197
198 my @before = map {
199 my $to_run = $Class::C3::Componentised::ApplyHooks::Before{$_};
200 ($to_run?[$_,$to_run]:())
201 } @heritage;
202
203 for my $todo (@before) {
204 my ($parent, $fn) = @$todo;
205 for my $f (reverse @$fn) {
206 $target->$f($parent)
207 }
208 }
20169807 209
e6b8b400 210 unshift ( @{"${target}::ISA"}, $comp );
211
212 my @after = map {
213 my $to_run = $Class::C3::Componentised::ApplyHooks::After{$_};
214 ($to_run?[$_,$to_run]:())
215 } @heritage;
216
217 for my $todo (reverse @after) {
218 my ($parent, $fn) = @$todo;
219 for my $f (@$fn) {
220 $target->$f($parent)
221 }
222 }
223 }
224 }
20169807 225}
d288ce53 226
078742b1 227=head2 load_optional_class
228
229Returns a true value if the specified class is installed and loaded
230successfully, throws an exception if the class is found but not loaded
231successfully, and false if the class is not installed
232
233=cut
234
235sub load_optional_class {
236 my ($class, $f_class) = @_;
3a4635fb 237
238 # ensure_class_loaded either returns a () (*not* true) or throws
239 eval {
240 $class->ensure_class_loaded($f_class);
241 1;
242 } && return 1;
243
dfb3a821 244 my $err = $@; # so we don't lose it
3a4635fb 245
eac9b176 246 if ($f_class =~ $invalid_class) {
247 $err = "Invalid class name '$f_class'";
248 }
249 else {
250 my $fn = quotemeta( (join ('/', split ('::', $f_class) ) ) . '.pm' );
251 return 0 if ($err =~ /Can't locate ${fn} in \@INC/ );
3a4635fb 252 }
eac9b176 253
254 if ($class->can('throw_exception')) {
3a4635fb 255 $class->throw_exception($err);
078742b1 256 }
dfb3a821 257 else {
3a4635fb 258 die $err;
dfb3a821 259 }
078742b1 260}
261
025a7b58 262=head1 AUTHORS
d288ce53 263
025a7b58 264Matt S. Trout and the L<DBIx::Class team|DBIx::Class/CONTRIBUTORS>
20169807 265
266Pulled out into seperate module by Ash Berlin C<< <ash@cpan.org> >>
d288ce53 267
025a7b58 268Optimizations and overall bolt-tightening by Peter "ribasushi" Rabbitson
269C<< <ribasushi@cpan.org> >>
270
271=head1 COPYRIGHT
272
273Copyright (c) 2006 - 2011 the Class::C3::Componentised L</AUTHORS> as listed
274above.
275
d288ce53 276=head1 LICENSE
277
278You may distribute this code under the same terms as Perl itself.
20169807 279
280=cut
281
2821;