File Coverage

blib/lib/Class/Method/Modifiers.pm
Criterion Covered Total %
statement 128 143 89.5
branch 48 62 77.4
condition 4 5 80.0
subroutine 26 26 100.0
pod 5 5 100.0
total 211 241 87.5


line stmt bran cond sub pod time code
1 38     38   1959417 use strict;
  37         503  
  33         1510  
2 35     31   229 use warnings;
  29         98  
  31         1812  
3             package Class::Method::Modifiers; # git description: v2.14-6-gede37cf
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.15';
9              
10 30     29   248 use base 'Exporter';
  34         167  
  34         8006  
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 34 100   28   6515 *_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 116     116 1 2287 my $into = shift;
31 116         363 my $type = shift;
32 116         199 my $code = pop;
33 116         325 my @names = @_;
34              
35 116 100       391 @names = @{ $names[0] } if ref($names[0]) eq 'ARRAY';
  2         7  
36              
37 116 100       329 return _fresh($into, $code, @names) if $type eq 'fresh';
38              
39 110         268 for my $name (@names) {
40 113 100       874 my $hit = $into->can($name) or do {
41 4         22 require Carp;
42 4         710 Carp::confess("The method '$name' is not found in the inheritance hierarchy for class $into");
43             };
44              
45 109         319 my $qualified = $into.'::'.$name;
46 109   100     662 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 109 100       301 if (!exists($cache->{"orig"})) {
54 33     28   403 no strict 'refs';
  32         3847  
  30         7405  
55              
56             # grab the original method (or undef if the method is inherited)
57 59         108 $cache->{"orig"} = *{$qualified}{CODE};
  59         211  
58              
59             # the "innermost" method, the one that "around" will ultimately wrap
60 59   66     281 $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 109 100       282 if ($type eq 'after') {
75 30         54 push @{ $cache->{$type} }, $code;
  30         109  
76             }
77             else {
78 79         129 unshift @{ $cache->{$type} }, $code;
  79         229  
79             }
80              
81             # wrap the method with another layer of around. much simpler than
82             # the Moose equivalent. :)
83 109 100       309 if ($type eq 'around') {
84 48         99 my $method = $cache->{wrapped};
85 48         127 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 48         3513 $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 109 100       240 if (@{ $cache->{$type} } == 1) {
  109         7407  
95              
96             # avoid these hash lookups every method invocation
97 87         228 my $before = $cache->{"before"};
98 87         152 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 87         203 my $wrapped = \$cache->{"wrapped"};
103              
104 87         215 my $attrs = _sub_attrs($cache->{wrapped});
105              
106 87         441 my $generated = "package $into;\n";
107 87         232 $generated .= "sub $name $attrs {";
108              
109             # before is easy, it doesn't affect the return value(s)
110 87 100       255 if (@$before) {
111 47         112 $generated .= '
112             for my $method (@$before) {
113             $method->(@_);
114             }
115             ';
116             }
117              
118 87 100       210 if (@$after) {
119 29         88 $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 58         116 $generated .= '$$wrapped->(@_);';
141             }
142              
143 87         145 $generated .= '}';
144              
145 29     28   251 no strict 'refs';
  29         72  
  28         1031  
146 28     28   187 no warnings 'redefine';
  28         72  
  29         1156  
147 29     28   238 no warnings 'closure';
  29         102  
  29         11064  
148 87 0   2   21668 eval $generated;
  2 0   32   117  
  0 0   11   0  
  0 0   5   0  
  0 100   6   0  
  0 100   3   0  
  0 100   2   0  
  0 100   2   0  
  0 50       0  
  0 100       0  
  0 100       0  
  0 50       0  
  32 50       13154  
  34 50       5093  
  23 50       1799  
  22 50       3270  
  8         748  
  4         79  
  6         1621  
  22         232  
  22         554  
  26         1972  
  17         435  
  7         1336  
  3         48  
  3         125  
  3         57  
  2         9  
  0         0  
  0         0  
  3         27  
  1         3  
  3         52  
  3         31  
  2         48  
  2         385  
  2         9  
  0         0  
  0         0  
  2         7  
  0         0  
  2         47  
  2         5  
  2         58  
  2         903  
149             };
150             }
151             }
152              
153             sub before {
154 30     30 1 6125 _install_modifier(scalar(caller), 'before', @_);
155             }
156              
157             sub after {
158 31     31 1 7257 _install_modifier(scalar(caller), 'after', @_);
159             }
160              
161             sub around {
162 49     49 1 10204 _install_modifier(scalar(caller), 'around', @_);
163             }
164              
165             sub fresh {
166 6     6 1 3063 my $code = pop;
167 6         15 my @names = @_;
168              
169 6 100       22 @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   39 my ($into, $code, @names) = @_;
176              
177 12         23 for my $name (@names) {
178 14 100       73 if ($name !~ /\A [a-zA-Z_] [a-zA-Z0-9_]* \z/xms) {
179 2         11 require Carp;
180 2         211 Carp::confess("Invalid method name '$name'");
181             }
182 12 100       95 if ($into->can($name)) {
183 4         24 require Carp;
184 4         513 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       20 if (_is_in_package($code, $into)) {
196 28     28   210 no strict 'refs';
  28         69  
  28         1525  
197 4         6 *{"$into\::$name"} = $code;
  4         105  
198             }
199             else {
200 28     28   185 no warnings 'closure'; # for 5.8.x
  28         65  
  28         7002  
201 4         8 my $attrs = _sub_attrs($code);
202 4         1974 eval "package $into; sub $name $attrs { \$code->(\@_) }";
203             }
204             }
205             }
206              
207             sub _sub_attrs {
208 139     139   292 my ($coderef) = @_;
209 139         366 local *_sub = $coderef;
210 139         225 local $@;
211 139         531 local $SIG{__DIE__};
212             # this assignment will fail to compile if it isn't an lvalue sub. we
213             # never want to actually call the sub though, so we return early.
214 139 100       7521 (eval 'return 1; &_sub = 1') ? ':lvalue' : '';
215             }
216              
217             sub _is_in_package {
218 8     8   16 my ($coderef, $package) = @_;
219 8         34 require B;
220 8         24 my $cv = B::svref_2object($coderef);
221 8         96 return $cv->GV->STASH->NAME eq $package;
222             }
223              
224             1;
225              
226             __END__