File Coverage

blib/lib/Class/Method/Modifiers.pm
Criterion Covered Total %
statement 128 142 90.1
branch 48 62 77.4
condition 4 5 80.0
subroutine 25 26 96.1
pod 5 5 100.0
total 210 240 87.5


line stmt bran cond sub pod time code
1 38     38   1683901 use strict;
  36         425  
  34         1546  
2 34     30   202 use warnings;
  28         70  
  28         1486  
3             package Class::Method::Modifiers; # git description: v2.12-17-gbc38636
4             # ABSTRACT: Provides Moose-like method modifiers
5             # KEYWORDS: method wrap modification patch
6             # vim: set ts=8 sts=4 sw=4 tw=115 et :
7              
8             our $VERSION = '2.13';
9              
10 27     28   177 use base 'Exporter';
  31         191  
  31         6743  
11              
12             our @EXPORT = qw(before after around);
13             our @EXPORT_OK = (@EXPORT, qw(fresh install_modifier));
14             our %EXPORT_TAGS = (
15             moose => [qw(before after around)],
16             all => \@EXPORT_OK,
17             );
18              
19             BEGIN {
20 33 100   27   5513 *_HAS_READONLY = $] >= 5.008 ? sub(){1} : sub(){0};
21             }
22              
23             our %MODIFIER_CACHE;
24              
25             # for backward compatibility
26             sub _install_modifier; # -w
27             *_install_modifier = \&install_modifier;
28              
29             sub install_modifier {
30 113     113 1 2959 my $into = shift;
31 113         318 my $type = shift;
32 113         174 my $code = pop;
33 113         262 my @names = @_;
34              
35 113 100       338 @names = @{ $names[0] } if ref($names[0]) eq 'ARRAY';
  2         7  
36              
37 113 100       283 return _fresh($into, $code, @names) if $type eq 'fresh';
38              
39 107         214 for my $name (@names) {
40 110 100       796 my $hit = $into->can($name) or do {
41 4         23 require Carp;
42 4         687 Carp::confess("The method '$name' is not found in the inheritance hierarchy for class $into");
43             };
44              
45 106         284 my $qualified = $into.'::'.$name;
46 106   100     639 my $cache = $MODIFIER_CACHE{$into}{$name} ||= {
47             before => [],
48             after => [],
49             around => [],
50             };
51              
52             # this must be the first modifier we're installing
53 106 100       286 if (!exists($cache->{"orig"})) {
54 32     27   390 no strict 'refs';
  31         3645  
  29         6518  
55              
56             # grab the original method (or undef if the method is inherited)
57 56         86 $cache->{"orig"} = *{$qualified}{CODE};
  56         182  
58              
59             # the "innermost" method, the one that "around" will ultimately wrap
60 56   66     227 $cache->{"wrapped"} = $cache->{"orig"} || $hit; #sub {
61             # # we can't cache this, because new methods or modifiers may be
62             # # added between now and when this method is called
63             # for my $package (@{ mro::get_linear_isa($into) }) {
64             # next if $package eq $into;
65             # my $code = *{$package.'::'.$name}{CODE};
66             # goto $code if $code;
67             # }
68             # require Carp;
69             # Carp::confess("$qualified\::$name disappeared?");
70             #};
71             }
72              
73             # keep these lists in the order the modifiers are called
74 106 100       236 if ($type eq 'after') {
75 30         52 push @{ $cache->{$type} }, $code;
  30         69  
76             }
77             else {
78 76         107 unshift @{ $cache->{$type} }, $code;
  76         194  
79             }
80              
81             # wrap the method with another layer of around. much simpler than
82             # the Moose equivalent. :)
83 106 100       255 if ($type eq 'around') {
84 45         79 my $method = $cache->{wrapped};
85 45         104 my $attrs = _sub_attrs($code);
86             # a bare "sub :lvalue {...}" will be parsed as a label and an
87             # indirect method call. force it to be treated as an expression
88             # using +
89 45         2987 $cache->{wrapped} = eval "package $into; +sub $attrs { \$code->(\$method, \@_); };";
90             }
91              
92             # install our new method which dispatches the modifiers, but only
93             # if a new type was added
94 106 100       190 if (@{ $cache->{$type} } == 1) {
  106         6149  
95              
96             # avoid these hash lookups every method invocation
97 84         141 my $before = $cache->{"before"};
98 84         181 my $after = $cache->{"after"};
99              
100             # this is a coderef that changes every new "around". so we need
101             # to take a reference to it. better a deref than a hash lookup
102 84         155 my $wrapped = \$cache->{"wrapped"};
103              
104 84         202 my $attrs = _sub_attrs($cache->{wrapped});
105              
106 84         286 my $generated = "package $into;\n";
107 84         316 $generated .= "sub $name $attrs {";
108              
109             # before is easy, it doesn't affect the return value(s)
110 84 100       217 if (@$before) {
111 47         87 $generated .= '
112             for my $method (@$before) {
113             $method->(@_);
114             }
115             ';
116             }
117              
118 84 100       184 if (@$after) {
119 29         68 $generated .= '
120             my $ret;
121             if (wantarray) {
122             $ret = [$$wrapped->(@_)];
123             '.(_HAS_READONLY ? 'Internals::SvREADONLY(@$ret, 1);' : '').'
124             }
125             elsif (defined wantarray) {
126             $ret = \($$wrapped->(@_));
127             }
128             else {
129             $$wrapped->(@_);
130             }
131              
132             for my $method (@$after) {
133             $method->(@_);
134             }
135              
136             wantarray ? @$ret : $ret ? $$ret : ();
137             '
138             }
139             else {
140 55         91 $generated .= '$$wrapped->(@_);';
141             }
142              
143 84         135 $generated .= '}';
144              
145 28     27   227 no strict 'refs';
  28         55  
  27         788  
146 27     27   136 no warnings 'redefine';
  27         57  
  28         1084  
147 28     27   213 no warnings 'closure';
  28         74  
  28         9578  
148 84 0   0   19507 eval $generated;
  0 0   29   0  
  0 0   15   0  
  0 0   4   0  
  0 100   6   0  
  0 100   4   0  
  0 100   2   0  
  0 100   2   0  
  0 50       0  
  0 100       0  
  0 100       0  
  0 50       0  
  29 50       9846  
  23 50       3120  
  17 50       1160  
  17 50       3986  
  6         933  
  3         64  
  7         644  
  16         202  
  16         407  
  14         1490  
  13         311  
  14         1862  
  15         116  
  8         149  
  8         67  
  2         8  
  2         6  
  2         45  
  10         26  
  8         159  
  13         180  
  8         180  
  2         61  
  2         543  
  2         8  
  0         0  
  0         0  
  2         9  
  0         0  
  2         46  
  2         5  
  2         50  
  2         884  
149             };
150             }
151             }
152              
153             sub before {
154 30     30 1 5092 _install_modifier(scalar(caller), 'before', @_);
155             }
156              
157             sub after {
158 31     31 1 7259 _install_modifier(scalar(caller), 'after', @_);
159             }
160              
161             sub around {
162 46     46 1 8993 _install_modifier(scalar(caller), 'around', @_);
163             }
164              
165             sub fresh {
166 6     6 1 3707 my $code = pop;
167 6         18 my @names = @_;
168              
169 6 100       20 @names = @{ $names[0] } if ref($names[0]) eq 'ARRAY';
  1         2  
170              
171 6         18 _fresh(scalar(caller), $code, @names);
172             }
173              
174             sub _fresh {
175 12     12   29 my ($into, $code, @names) = @_;
176              
177 12         24 for my $name (@names) {
178 14 100       77 if ($name !~ /\A [a-zA-Z_] [a-zA-Z0-9_]* \z/xms) {
179 2         10 require Carp;
180 2         219 Carp::confess("Invalid method name '$name'");
181             }
182 12 100       83 if ($into->can($name)) {
183 4         23 require Carp;
184 4         536 Carp::confess("Class $into already has a method named '$name'");
185             }
186              
187             # We need to make sure that the installed method has its CvNAME in
188             # the appropriate package; otherwise, it would be subject to
189             # deletion if callers use namespace::autoclean. If $code was
190             # compiled in the target package, we can just install it directly;
191             # otherwise, we'll need a different approach. Using Sub::Name would
192             # be fine in all cases, at the cost of introducing a dependency on
193             # an XS-using, non-core module. So instead we'll use string-eval to
194             # create a new subroutine that wraps $code.
195 8 100       16 if (_is_in_package($code, $into)) {
196 27     27   206 no strict 'refs';
  27         47  
  27         1277  
197 4         8 *{"$into\::$name"} = $code;
  4         100  
198             }
199             else {
200 27     27   149 no warnings 'closure'; # for 5.8.x
  27         51  
  27         5497  
201 4         10 my $attrs = _sub_attrs($code);
202 4         1996 eval "package $into; sub $name $attrs { \$code->(\@_) }";
203             }
204             }
205             }
206              
207             sub _sub_attrs {
208 133     133   240 my ($coderef) = @_;
209 133         317 local *_sub = $coderef;
210 133         210 local $@;
211             # this assignment will fail to compile if it isn't an lvalue sub. we
212             # never want to actually call the sub though, so we return early.
213 133 100       5906 (eval 'return 1; &_sub = 1') ? ':lvalue' : '';
214             }
215              
216             sub _is_in_package {
217 8     8   17 my ($coderef, $package) = @_;
218 8         38 require B;
219 8         21 my $cv = B::svref_2object($coderef);
220 8         107 return $cv->GV->STASH->NAME eq $package;
221             }
222              
223             1;
224              
225             __END__