File Coverage

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


line stmt bran cond sub pod time code
1 38     38   1983200 use strict;
  38         1388  
  34         1541  
2 36     31   278 use warnings;
  29         95  
  31         1815  
3             package Class::Method::Modifiers; # git description: v2.13-4-g6e49964
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.14';
9              
10 30     29   257 use base 'Exporter';
  35         200  
  35         7999  
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 35 100   28   6581 *_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 2264 my $into = shift;
31 116         378 my $type = shift;
32 116         188 my $code = pop;
33 116         269 my @names = @_;
34              
35 116 100       369 @names = @{ $names[0] } if ref($names[0]) eq 'ARRAY';
  2         6  
36              
37 116 100       325 return _fresh($into, $code, @names) if $type eq 'fresh';
38              
39 110         231 for my $name (@names) {
40 113 100       889 my $hit = $into->can($name) or do {
41 4         24 require Carp;
42 4         798 Carp::confess("The method '$name' is not found in the inheritance hierarchy for class $into");
43             };
44              
45 109         298 my $qualified = $into.'::'.$name;
46 109   100     665 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       312 if (!exists($cache->{"orig"})) {
54 34     28   413 no strict 'refs';
  32         5199  
  30         7571  
55              
56             # grab the original method (or undef if the method is inherited)
57 59         104 $cache->{"orig"} = *{$qualified}{CODE};
  59         216  
58              
59             # the "innermost" method, the one that "around" will ultimately wrap
60 59   66     250 $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       276 if ($type eq 'after') {
75 30         44 push @{ $cache->{$type} }, $code;
  30         90  
76             }
77             else {
78 79         140 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       315 if ($type eq 'around') {
84 48         92 my $method = $cache->{wrapped};
85 48         118 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         3641 $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       207 if (@{ $cache->{$type} } == 1) {
  109         7330  
95              
96             # avoid these hash lookups every method invocation
97 87         156 my $before = $cache->{"before"};
98 87         194 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         161 my $wrapped = \$cache->{"wrapped"};
103              
104 87         211 my $attrs = _sub_attrs($cache->{wrapped});
105              
106 87         328 my $generated = "package $into;\n";
107 87         334 $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         88 $generated .= '
112             for my $method (@$before) {
113             $method->(@_);
114             }
115             ';
116             }
117              
118 87 100       215 if (@$after) {
119 29         84 $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         111 $generated .= '$$wrapped->(@_);';
141             }
142              
143 87         147 $generated .= '}';
144              
145 29     28   574 no strict 'refs';
  29         73  
  28         1046  
146 28     28   183 no warnings 'redefine';
  28         85  
  29         1148  
147 29     28   256 no warnings 'closure';
  29         82  
  29         11232  
148 87 0   2   22674 eval $generated;
  2 0   32   110  
  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       10954  
  31 50       3812  
  20 50       1511  
  23 50       3148  
  6         579  
  4         86  
  8         921  
  21         200  
  21         546  
  22         1364  
  16         444  
  8         1746  
  5         19  
  5         122  
  3         59  
  2         6  
  0         0  
  0         0  
  3         28  
  1         3  
  3         45  
  3         32  
  2         52  
  2         403  
  2         7  
  0         0  
  0         0  
  2         7  
  0         0  
  2         47  
  2         5  
  2         49  
  2         941  
149             };
150             }
151             }
152              
153             sub before {
154 30     30 1 5861 _install_modifier(scalar(caller), 'before', @_);
155             }
156              
157             sub after {
158 31     31 1 7201 _install_modifier(scalar(caller), 'after', @_);
159             }
160              
161             sub around {
162 49     49 1 10029 _install_modifier(scalar(caller), 'around', @_);
163             }
164              
165             sub fresh {
166 6     6 1 3028 my $code = pop;
167 6         16 my @names = @_;
168              
169 6 100       29 @names = @{ $names[0] } if ref($names[0]) eq 'ARRAY';
  1         3  
170              
171 6         17 _fresh(scalar(caller), $code, @names);
172             }
173              
174             sub _fresh {
175 12     12   27 my ($into, $code, @names) = @_;
176              
177 12         21 for my $name (@names) {
178 14 100       79 if ($name !~ /\A [a-zA-Z_] [a-zA-Z0-9_]* \z/xms) {
179 2         11 require Carp;
180 2         262 Carp::confess("Invalid method name '$name'");
181             }
182 12 100       96 if ($into->can($name)) {
183 4         24 require Carp;
184 4         575 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       18 if (_is_in_package($code, $into)) {
196 28     28   217 no strict 'refs';
  28         67  
  28         1566  
197 4         7 *{"$into\::$name"} = $code;
  4         100  
198             }
199             else {
200 28     28   192 no warnings 'closure'; # for 5.8.x
  28         75  
  28         6801  
201 4         10 my $attrs = _sub_attrs($code);
202 4         2129 eval "package $into; sub $name $attrs { \$code->(\@_) }";
203             }
204             }
205             }
206              
207             sub _sub_attrs {
208 139     139   280 my ($coderef) = @_;
209 139         330 local *_sub = $coderef;
210 139         222 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 139 100       6611 (eval 'return 1; &_sub = 1') ? ':lvalue' : '';
214             }
215              
216             sub _is_in_package {
217 8     8   18 my ($coderef, $package) = @_;
218 8         36 require B;
219 8         31 my $cv = B::svref_2object($coderef);
220 8         89 return $cv->GV->STASH->NAME eq $package;
221             }
222              
223             1;
224              
225             __END__