File Coverage

blib/lib/Role/Hooks.pm
Criterion Covered Total %
statement 189 265 71.7
branch 65 122 53.2
condition 54 88 61.3
subroutine 26 33 78.7
pod 4 4 100.0
total 338 512 66.2


line stmt bran cond sub pod time code
1 9     9   2129040 use 5.008001;
  9         74  
2 9     9   43 use strict;
  9         15  
  9         167  
3 9     9   35 use warnings;
  9         14  
  9         313  
4              
5              
6             use Class::Method::Modifiers qw( install_modifier );
7 9     9   3696  
  9         12433  
  9         1112  
8             our $AUTHORITY = 'cpan:TOBYINK';
9             our $VERSION = '0.006';
10              
11             our %CALLBACKS_BEFORE_APPLY;
12             our %CALLBACKS_AFTER_APPLY;
13             our %CALLBACKS_AFTER_INFLATE;
14             our %ARGS;
15              
16             BEGIN { *DEBUG = $ENV{'PERL_ROLE_HOOKS_DEBUG'} ? sub(){1} : sub(){0} };
17 9 50   9   2902  
18             # limited version of Safe::Isa
19             my $_isa = sub { ref( $_[0] ) and $_[0]->isa( $_[1] ) };
20              
21             my ($me, $msg, @args) = @_;
22             require Carp;
23 0     0   0 Carp::croak( @args ? sprintf($msg, @args) : $msg );
24 0         0 }
25 0 0       0  
26             my ($me, $msg, @args) = @_;
27             require Carp;
28             Carp::carp( @args ? sprintf($msg, @args) : $msg );
29 0     0   0 }
30 0         0  
31 0 0       0 my ($me, $msg, @args) = @_;
32             require Carp;
33             Carp::carp( @args ? sprintf($msg, @args) : $msg ) if DEBUG;
34             }
35 0     0   0  
36 0         0 my $target = pop;
37 0         0
38             if ($INC{'Role/Tiny.pm'}
39             and 'Role::Tiny'->can('is_role')
40             and 'Role::Tiny'->is_role($target)) {
41 95     95 1 131 return 'Role::Tiny';
42             }
43 95 100 66     478
      100        
44             # really old versions of Role::Tiny
45             if ($INC{'Role/Tiny.pm'}
46 46         336 and !'Role::Tiny'->can('is_role')
47             and $Role::Tiny::INFO{$target}) {
48             return 'Role::Tiny'; # uncoverable statement
49             }
50 49 0 66     241
      33        
51             if ($INC{'Moose/Meta/Role.pm'}
52             and do { require Moose::Util; 1 }
53 0         0 and Moose::Util::find_meta($target)->$_isa('Moose::Meta::Role')) {
54             return 'Moose::Role';
55             }
56 49 100 66     123
      66        
57 30         123 if ($INC{'Mouse/Meta/Role.pm'}
  30         110  
58             and do { require Mouse::Util; 1 }
59 26         73 and Mouse::Util::find_meta($target)->$_isa('Mouse::Meta::Role')) {
60             return 'Mouse::Role';
61             }
62 23 0 33     11366
      0        
63 0         0 if ($INC{'Role/Basic.pm'}
  0         0  
64             and eval { 'Role::Basic'->_load_role($target) }) {
65 0         0 return 'Role::Basic';
66             }
67            
68 23 50 33     62 no strict 'refs';
69 0         0 my $UM = ${"$target\::USES_MITE"};
70 0         0 if ( defined $UM and $UM eq 'Mite::Role' ) {
71             return 'Mite::Role';
72             }
73 9     9   62
  9         21  
  9         2225  
74 23         27 return undef;
  23         132  
75 23 100 100     77 }
76 13         26  
77             my ($me, $target, @callbacks) = @_;
78             return unless @callbacks;
79 10         44 $me->is_role($target) or $me->_croak('%s is not a role', $target);
80             $me->_install_patches($target);
81             push @{ $CALLBACKS_BEFORE_APPLY{$target}||=[] }, @callbacks;
82             return $me;
83 20     20 1 6392 }
84 20 100       55  
85 19 50       83 my ($me, $target, @callbacks) = @_;
86 19         71 return unless @callbacks;
87 19   100     25 $me->is_role($target) or $me->_croak('%s is not a role', $target);
  19         77  
88 19         41 $me->_install_patches($target);
89             push @{ $CALLBACKS_AFTER_APPLY{$target}||=[] }, @callbacks;
90             return $me;
91             }
92 19     19 1 131  
93 19 100       52 no warnings 'uninitialized';
94 18 50       43 my ($me, $target, @callbacks) = @_;
95 18         55 return unless @callbacks;
96 18   100     24 $me->is_role($target) eq 'Role::Tiny'
  18         87  
97 18         41 or $target->isa('Moo::Object')
98             or $me->_croak('%s is not a Moo class or role', $target);
99             $me->_install_patches($target);
100             $me->_install_patches_inflation($target);
101 9     9   65 push @{ $CALLBACKS_AFTER_INFLATE{$target}||=[] }, @callbacks;
  9         19  
  9         19420  
102 6     6 1 110 return $me;
103 6 100       18 }
104 3 50 66     11  
105             {
106             # Internals for monkey-patching role implementations.
107 3         9 #
108 3         9
109 3   50     6 my %patched;
  3         16  
110 3         8 my ($me, $target) = @_;
111            
112             if ($INC{'Role/Tiny.pm'}) {
113             $patched{'Role::Tiny'} ||= $me->_install_patches_roletiny;
114             }
115             if ($INC{'Moo/Role.pm'} or $INC{'Moo.pm'}) {
116             $patched{'Moo::Role'} ||= $me->_install_patches_moorole;
117             }
118             if ($INC{'Moose/Role.pm'} or $INC{'Moose.pm'}) {
119 40     40   90 $patched{'Moose::Role'} ||= $me->_install_patches_mooserole;
120             }
121 40 100       73 if ($INC{'Mouse/Role.pm'} or $INC{'Mouse.pm'}) {
122 27   66     83 $patched{'Mouse::Role'} ||= $me->_install_patches_mouserole;
123             }
124 40 100 100     121 if ($INC{'Role/Basic.pm'}) {
125 21   66     51 $patched{'Role::Basic'} ||= $me->_install_patches_rolebasic;
126             }
127 40 100 100     132
128 19   66     47 my $is_role = $me->is_role($target);
129             if ( defined $is_role and $is_role eq 'Mite::Role' ) {
130 40 50 33     183 $patched{'Mite::Role'}{$target} ||= $me->_install_patches_miterole($target);
131 0   0     0 }
132             }
133 40 50       74
134 0   0     0 my %patched_inflation;
135             my ($me, $target) = @_;
136             if ($INC{'Moo/Role.pm'} or $INC{'Moo.pm'}) {
137 40         74 $patched_inflation{'Moo::Role'} ||= $me->_install_patches_moorole_inflation;
138 40 100 100     145 }
139 6   66     26 }
140            
141             my ($me) = @_;
142             return 1 if $patched{'Role::Tiny'};
143            
144             $me->_debug("Installing patches for Role::Tiny") if DEBUG;
145 3     3   7
146 3 50 33     8 require Role::Tiny;
147 3   66     10
148             install_modifier 'Role::Tiny', around => 'role_application_steps', sub {
149             my $orig = shift;
150             my @steps = $orig->(@_);
151             return (
152 6     6   15 __PACKAGE__ . '::_run_role_tiny_before_callbacks',
153 6 50       17 @steps,
154             __PACKAGE__ . '::_run_role_tiny_after_callbacks',
155 6         8 );
156             };
157 6         532
158             *_run_role_tiny_before_callbacks = sub {
159             my (undef, $to, $role) = @_;
160 8     8   422 $me->_debug("Calling role hooks for $role before application to $to") if DEBUG;
161 8         23 my @callbacks = @{ $CALLBACKS_BEFORE_APPLY{$role} || [] };
162             for my $cb (@callbacks) {
163 8         58 $cb->($role, $to);
164             }
165             return;
166             };
167 6         3628
168             *_run_role_tiny_after_callbacks = sub {
169             my (undef, $to, $role) = @_;
170 8     8   52 $me->_debug("Calling role hooks for $role after application to $to") if DEBUG;
171 8         11 my @callbacks = @{ $CALLBACKS_AFTER_APPLY{$role} || [] };
172 8 100       11 for my $cb (@callbacks) {
  8         37  
173 8         20 $cb->($role, $to);
174 9         42 }
175             if (my $is_role = $me->is_role($to)) {
176 8         50 $me->_debug("Copying role hooks for $role to $to") if DEBUG;
177 6         1807 $me->before_apply($to, @{ $CALLBACKS_BEFORE_APPLY{$role} || [] });
178             $me->after_apply($to, @{ $CALLBACKS_AFTER_APPLY{$role} || [] });
179             if ($is_role eq 'Role::Tiny' or $to->isa('Moo::Object')) {
180 8     8   23909 $me->after_inflate($to, @{ $CALLBACKS_AFTER_INFLATE{$role} || [] });
181 8         22 }
182 8 100       14 }
  8         45  
183 8         20 return;
184 9         45 };
185            
186 8 100       53 return 1;
187 4         8 }
188 4 100       7
  4         19  
189 4 100       9 my ($me) = @_;
  4         19  
190 4 50 33     15 $patched{'Role::Tiny'} ||= $me->_install_patches_roletiny;
191 4 100       7 return 1 if $patched{'Moo::Role'};
  4         42  
192            
193             $me->_debug("Installing patches for Moo::Role") if DEBUG;
194 8         25
195 6         28 require Moo::Role;
196             require List::Util;
197 6         24
198             # Mostly can just rely on Role::Tiny, but need
199             # to move _run_callbacks_before_apply to the
200             # front of the queue!
201 4     4   10 #
202 4   66     14 install_modifier 'Moo::Role', around => 'role_application_steps', sub {
203 4 50       13 my $orig = shift;
204             my @steps = $orig->(@_);
205 4         6 return List::Util::uniqstr(
206             __PACKAGE__ . '::_run_role_tiny_before_callbacks',
207 4         501 @steps,
208 4         3281 );
209             };
210            
211             return 1;
212             }
213            
214             my ($me) = @_;
215 5     5   2175 return 1 if $patched_inflation{'Moo::Role'};
216 5         17
217 5         91 $me->_debug("Installing inflation patches for Moo::Role") if DEBUG;
218            
219             require Moo::HandleMoose;
220            
221 4         43 install_modifier 'Moo::HandleMoose', after => 'inject_real_metaclass_for', sub {
222             my ( $name ) = @_;
223 4         1061 $me->_run_moo_inhale_callbacks( $name );
224             };
225            
226             my %already;
227 1     1   2 *_run_moo_inhale_callbacks = sub {
228 1 50       3 my (undef, $name) = @_;
229             $me->_debug("Calling role hooks for $name after inflation") if DEBUG;
230 1         1 my @callbacks = @{ $CALLBACKS_AFTER_INFLATE{$name} || [] };
231             for my $cb (@callbacks) {
232 1         424 next if $already{"$name|$cb"}++;
233             $cb->($name);
234             }
235 4     4   434770 };
236 4         15
237 1         6059 return 1;
238             }
239 1         238
240             my ($me) = @_;
241 4     4   8 return 1 if $patched{'Moose::Role'};
242 4         5
243 4 50       6 $me->_debug("Installing patches for Moose::Role") if DEBUG;
  4         17  
244 4         6
245 4 100       32 require Moose::Meta::Role;
246 3         10
247             install_modifier 'Moose::Meta::Role', around => 'apply', sub {
248 1         6 my ($orig, $role_meta, $to_meta, %args) = @_;
249             local *ARGS = \%args;
250 1         6 my $role = $role_meta->name;
251             my $to = $to_meta->name;
252             do {
253             $me->_debug("Calling role hooks for $role before application to $to") if DEBUG;
254 4     4   11 my @callbacks = @{ $CALLBACKS_BEFORE_APPLY{$role} || [] };
255 4 50       11 for my $cb (@callbacks) {
256             $cb->($role, $to);
257 4         6 }
258             };
259 4         19 my $application = $role_meta->$orig($to_meta, %args);
260             do {
261             $me->_debug("Calling role hooks for $role after application to $to") if DEBUG;
262 4     4   4808 my @callbacks = @{ $CALLBACKS_AFTER_APPLY{$role} || [] };
263 4         12 for my $cb (@callbacks) {
264 4         25 $cb->($role, $to);
265 4         19 }
266 4         7 };
267 4         5 if ($me->is_role($to)) {
268 4 50       7 $me->_debug("Copying role hooks for $role to $to") if DEBUG;
  4         15  
269 4         11 $me->before_apply($to, @{ $CALLBACKS_BEFORE_APPLY{$role} || [] });
270 6         20 $me->after_apply($to, @{ $CALLBACKS_AFTER_APPLY{$role} || [] });
271             }
272             return $application;
273 4         38 };
274 4         5259
275 4         6 return 1;
276 4 50       6 }
  4         17  
277 4         10
278 6         22 my ($me) = @_;
279             return 1 if $patched{'Mouse::Role'};
280            
281 4 100       30 $me->_debug("Installing patches for Mouse::Role") if DEBUG;
282 2         2
283 2 50       6 require Mouse::Meta::Role;
  2         8  
284 2 50       5
  2         7  
285             install_modifier 'Mouse::Meta::Role', around => 'apply', sub {
286 4         23 my ($orig, $role_meta, $to_meta, %args) = @_;
287 4         30 local *ARGS = \%args;
288             my $role = $role_meta->name;
289 4         1102 my $to = $to_meta->name;
290             do {
291             $me->_debug("Calling role hooks for $role before application to $to") if DEBUG;
292             my @callbacks = @{ $CALLBACKS_BEFORE_APPLY{$role} || [] };
293 0     0   0 for my $cb (@callbacks) {
294 0 0       0 $cb->($role, $to);
295             }
296 0         0 };
297             my $application = $role_meta->$orig($to_meta, %args);
298 0         0 do {
299             $me->_debug("Calling role hooks for $role after application to $to") if DEBUG;
300             my @callbacks = @{ $CALLBACKS_AFTER_APPLY{$role} || [] };
301 0     0   0 for my $cb (@callbacks) {
302 0         0 $cb->($role, $to);
303 0         0 }
304 0         0 };
305 0         0 if ($me->is_role($to)) {
306 0         0 $me->_debug("Copying role hooks for $role to $to") if DEBUG;
307 0 0       0 $me->before_apply($to, @{ $CALLBACKS_BEFORE_APPLY{$role} || [] });
  0         0  
308 0         0 $me->after_apply($to, @{ $CALLBACKS_AFTER_APPLY{$role} || [] });
309 0         0 }
310             return $application;
311             };
312 0         0
313 0         0 return 1;
314 0         0 }
315 0 0       0
  0         0  
316 0         0 my ($me) = @_;
317 0         0 return 1 if $patched{'Role::Basic'};
318            
319             $me->_debug("Installing patches for Role::Basic") if DEBUG;
320 0 0       0
321 0         0 require Role::Basic;
322 0 0       0
  0         0  
323 0 0       0 $me->_carp("Role::Hooks is only tested with Role::Basic 0.07 to 0.13")
  0         0  
324             unless $Role::Basic::VERSION =~ /^0\.(?:0[7-9]|1[0-3])/;
325 0         0
326 0         0 install_modifier 'Role::Basic', around => '_add_role_methods_to_target', sub {
327             my ($orig, $rb, $role, $to, $modifiers) = @_;
328 0         0 local *ARGS = $modifiers;
329             do {
330             $me->_debug("Calling role hooks for $role before application to $to") if DEBUG;
331             my @callbacks = @{ $CALLBACKS_BEFORE_APPLY{$role} || [] };
332 0     0   0 for my $cb (@callbacks) {
333 0 0       0 $cb->($role, $to);
334             }
335 0         0 };
336             my $application = $rb->$orig($role, $to, $modifiers);
337 0         0 do {
338             $me->_debug("Calling role hooks for $role after application to $to") if DEBUG;
339 0 0       0 my @callbacks = @{ $CALLBACKS_AFTER_APPLY{$role} || [] };
340             for my $cb (@callbacks) {
341             $cb->($role, $to);
342             }
343 0     0   0 };
344 0         0 if ($me->is_role($to)) {
345 0         0 $me->_debug("Copying role hooks for $role to $to") if DEBUG;
346 0         0 $me->before_apply($to, @{ $CALLBACKS_BEFORE_APPLY{$role} || [] });
347 0 0       0 $me->after_apply($to, @{ $CALLBACKS_AFTER_APPLY{$role} || [] });
  0         0  
348 0         0 }
349 0         0 return $application;
350             };
351            
352 0         0 return 1;
353 0         0 }
354 0         0
355 0 0       0 my ($me, $target) = @_;
  0         0  
356 0         0 return 1 if $ENV{MITE_COMPILE};
357 0         0 install_modifier $target, around => '__FINALIZE_APPLICATION__', sub {
358             my ($orig, $role, $to, $modifiers) = @_;
359             local *ARGS = $modifiers || {};
360 0 0       0 my $indirect = ( $modifiers || {} )->{-indirect};
361 0         0 if ( not $indirect ) {
362 0 0       0 $me->_debug("Calling role hooks for $role before application to $to") if DEBUG;
  0         0  
363 0 0       0 my @callbacks = @{ $CALLBACKS_BEFORE_APPLY{$role} || [] };
  0         0  
364             for my $cb (@callbacks) {
365 0         0 $cb->($role, $to);
366 0         0 }
367             }
368 0         0 $role->$orig($to, $modifiers);
369             if ( not $indirect ) {
370             $me->_debug("Calling role hooks for $role after application to $to") if DEBUG;
371             my @callbacks = @{ $CALLBACKS_AFTER_APPLY{$role} || [] };
372 2     2   5 for my $cb (@callbacks) {
373 2 50       6 $cb->($role, $to);
374             }
375 3     3   1425 }
376 3   100     13 my $to_type = $me->is_role($to);
377 3   100     19 if (defined $to_type and $to_type eq 'Mite::Role') {
378 3 100       8 $me->_debug("Copying role hooks for $role to $to") if DEBUG;
379 2         3 $me->before_apply($to, @{ $CALLBACKS_BEFORE_APPLY{$role} || [] });
380 2 50       2 $me->after_apply($to, @{ $CALLBACKS_AFTER_APPLY{$role} || [] });
  2         7  
381 2         6 }
382 3         12 };
383             1;
384             }
385 3         21 }
386 3 100       47  
387 2         2 1;
388 2 50       3  
  2         7  
389 2         3  
390 3         12 =pod
391              
392             =encoding utf-8
393 3         14  
394 3 100 66     12 =head1 NAME
395 1         1  
396 1 50       2 Role::Hooks - role callbacks
  1         4  
397 1 50       2  
  1         5  
398             =head1 SYNOPSIS
399 2         19  
400 2         557 package Local::Role {
401             use Moo::Role;
402             use Role::Hooks;
403            
404             Role::Hooks->after_apply(__PACKAGE__, sub {
405             my ($role, $target) = @_;
406             print "$role has been applied to $target.\n";
407             });
408             }
409            
410             package Local::Class {
411             use Moo;
412             with "Local::Role"; # prints above message
413             }
414              
415             =head1 DESCRIPTION
416              
417             This module allows a role to run a callback when it is applied to a class or
418             to another role.
419              
420             =head2 Compatibility
421              
422             It should work with L<Role::Tiny>, L<Moo::Role>, L<Moose::Role>,
423             L<Mouse::Role>, L<Role::Basic>, and L<Mite>. Not all class builders work well
424             with all role builders (for example, a Moose class consuming a Mouse role).
425             But when they do work together, Role::Hooks should be able to run the
426             callbacks. (The only combination I've tested is Moo with Moose though.)
427              
428             Some other role implementations (such as L<Moos::Role>, L<exact::role>,
429             and L<OX::Role>) are just wrappers around one of the supported role builders,
430             so should mostly work.
431              
432             With Role::Basic, the C<after_apply> hook is called a little earlier than
433             would be ideal; after the role has been fully loaded and its methods have
434             been copied into the target package, but before handling C<requires>, and
435             before patching the C<DOES> method in the target package. If you are using
436             Role::Basic, consider switching to Role::Tiny.
437              
438             With Mite, the C<before_apply> hook is called fairly late; after the role
439             is fully loaded and attributes and methods have been copied into the target
440             package, after C<DOES> has been patched, but before method modifiers from the
441             role have been applied to the target package.
442              
443             Apart from Role::Tiny/Moo::Role, a hashref of additional arguments (things
444             like "-excludes" and "-alias") can be passed when consuming a role. Although
445             I discourage people from using these in general, if you need access to
446             these arguments in the callback, you can check C<< %Role::Hooks::ARGS >>.
447              
448             Roles generated via L<Package::Variant> should work; see
449             F<t/20packagevariant.t> for a demonstration.
450              
451             =head2 Methods
452              
453             =over
454              
455             =item C<< before_apply >>
456              
457             Role::Hooks->before_apply($rolename, $callback);
458              
459             Sets up a callback for a role that will be called before the role is applied
460             to a target package. The callback will be passed two parameters: the role
461             being applied and the target package.
462              
463             The role being applied may not be the same role as the role the callback was
464             defined in!
465              
466             package Local::Role1 {
467             use Moo::Role;
468             use Role::Hooks;
469             Role::Hooks->before_apply(__PACKAGE__, sub {
470             my ($role, $target) = @_;
471             print "$role has been applied to $target.\n";
472             });
473             }
474            
475             package Local::Role2 {
476             use Moo::Role;
477             with "Local::Role1";
478             }
479            
480             package Local::Class1 {
481             use Moo::Role;
482             with "Local::Role2";
483             }
484              
485             This will print:
486              
487             Local::Role1 has been applied to Local::Role2.
488             Local::Role2 has been applied to Local::Class1.
489              
490             If you only care about direct applications of roles (i.e. the first one):
491              
492             Role::Hooks->before_apply(__PACKAGE__, sub {
493             my ($role, $target) = @_;
494             return if $role ne __PACKAGE__;
495             print "$role has been applied to $target.\n";
496             });
497              
498             If you only care about roles being applied to classes (i.e. the second one):
499              
500             Role::Hooks->before_apply(__PACKAGE__, sub {
501             my ($role, $target) = @_;
502             return if Role::Hooks->is_role($target);
503             print "$role has been applied to $target.\n";
504             });
505              
506             =item C<< after_apply >>
507              
508             Role::Hooks->after_apply($rolename, $callback);
509              
510             The same as C<< before_apply >>, but called later in the role application
511             process.
512              
513             Note that when the callback is called, even though it's after the role has
514             been applied to the target, it doesn't mean the target has finished being
515             built. For example, there might be C<has> statements after the C<with>
516             statement, and those will not have been evaluated yet.
517              
518             If you want to throw an error when someone applies your role to an
519             inappropriate target, it is probably better to do that in C<before_apply> if
520             you can.
521              
522             =item C<< after_inflate >>
523              
524             Role::Hooks->after_inflate($pkg_name, $callback);
525              
526             Even though this is part of Role::Hooks, it works on classes too.
527             But it only works on classes and roles built using Moo. This runs
528             your callback if your Moo class or role gets "inflated" to a Moose
529             class or role.
530              
531             If you set up a callback for a role, then the callback will also
532             get called if any packages that role was applied to get inflated.
533              
534             =item C<< is_role >>
535              
536             Will return true if the given package seems to be a role, false otherwise.
537              
538             (In fact, returns a string representing which role builder the role seems
539             to be using -- "Role::Tiny", "Moose::Role", "Mouse::Role", "Role::Basic",
540             or "Mite::Role"; roles built using Moo::Role are detected as "Role::Tiny".)
541              
542             =back
543              
544             =head1 ENVIRONMENT
545              
546             The environment variable C<PERL_ROLE_HOOKS_DEBUG> may be set to true to
547             enable debugging messages.
548              
549             =head1 BUGS
550              
551             Please report any bugs to
552             L<http://rt.cpan.org/Dist/Display.html?Queue=Role-Hooks>.
553              
554             =head1 SEE ALSO
555              
556             L<Role::Tiny>, L<Moose::Role>.
557              
558             =head1 AUTHOR
559              
560             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
561              
562             =head1 COPYRIGHT AND LICENCE
563              
564             This software is copyright (c) 2020-2022 by Toby Inkster.
565              
566             This is free software; you can redistribute it and/or modify it under
567             the same terms as the Perl 5 programming language system itself.
568              
569             =head1 DISCLAIMER OF WARRANTIES
570              
571             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
572             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
573             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.