File Coverage

lib/Class/Simple/Readonly/Cached.pm
Criterion Covered Total %
statement 96 113 84.9
branch 47 64 73.4
condition 1 6 16.6
subroutine 8 9 88.8
pod 3 3 100.0
total 155 195 79.4


line stmt bran cond sub pod time code
1             package Class::Simple::Readonly::Cached;
2              
3 5     5   493703 use strict;
  5         32  
  5         116  
4 5     5   23 use warnings;
  5         9  
  5         101  
5 5     5   20 use Carp;
  5         8  
  5         211  
6 5     5   1676 use Class::Simple;
  5         18099  
  5         4054  
7              
8             my @ISA = ('Class::Simple');
9              
10             our %cached;
11              
12             =head1 NAME
13              
14             Class::Simple::Readonly::Cached - cache messages to an object
15              
16             =head1 VERSION
17              
18             Version 0.09
19              
20             =cut
21              
22             our $VERSION = '0.09';
23              
24             =head1 SYNOPSIS
25              
26             A sub-class of L<Class::Simple> which caches calls to read
27             the status of an object that are otherwise expensive.
28              
29             It is up to the caller to maintain the cache if the object comes out of sync with the cache,
30             for example by changing its state.
31              
32             You can use this class to create a caching layer to an object of any class
33             that works on objects which doesn't change its state based on input:
34              
35             use Class::Simple::Readonly::Cached;
36              
37             my $obj = Class::Simple->new();
38             $obj->val('foo');
39             $obj = Class::Simple::Readonly::Cached->new(object => $obj, cache => {});
40             my $val = $obj->val();
41             print "$val\n"; # Prints "foo"
42            
43             #... set $obj to be some other class which will take an argument 'a',
44             # with a value 'b'
45            
46             $val = $obj->val(a => 'b'); # You
47              
48             =head1 SUBROUTINES/METHODS
49              
50             =head2 new
51              
52             Creates a Class::Simple::Readonly::Cached object.
53              
54             It takes one mandatory parameter: cache,
55             which is either an object which understands clear(), get() and set() calls,
56             such as an L<CHI> object;
57             or is a reference to a hash where the return values are to be stored.
58              
59             It takes one optional argument: object,
60             which is an object which is taken to be the object to be cached.
61             If not given, an object of the class L<Class::Simple> is instantiated
62             and that is used.
63              
64             use Gedcom;
65              
66             my %hash;
67             my $person = Gedcom::Person->new();
68             ... # Set up some data
69             my $object = Class::Simple::Readonly::Cached(object => $person, cache => \%hash);
70             my $father1 = $object->father(); # Will call gedcom->father() to get the person's father
71             my $father2 = $object->father(); # Will retrieve the father from the cache without calling person->father()
72              
73             Takes one optional argument: quiet,
74             if you attempt to cache an object that is already cached, rather than create
75             another copy you receive a warning and the previous cached copy is returned.
76             The 'quiet' option, when non-zero, silences the warning.
77              
78             =cut
79              
80             sub new {
81 13     13 1 96094 my $proto = shift;
82 13   33     89 my $class = ref($proto) || $proto;
83              
84             # Use Class::Simple::Readonly::Cached->new(), not Class::Simple::Readonly::Cached::new()
85 13 50       31 if(!defined($class)) {
86 0         0 Carp::carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
87 0         0 return;
88             }
89              
90 13         18 my %args;
91 13 100       61 if(ref($_[0]) eq 'HASH') {
    50          
    50          
92 3         6 %args = %{$_[0]};
  3         9  
93             } elsif(ref($_[0])) {
94 0         0 Carp::carp('Usage: ', __PACKAGE__, '->new(cache => $cache [, object => $object ], %args)');
95 0         0 return;
96             } elsif(@_ % 2 == 0) {
97 10         31 %args = @_;
98             }
99              
100 13 100       35 if(!$args{'cache'}) {
101 3         21 Carp::carp('Usage: ', __PACKAGE__, '->new(cache => $cache [, object => $object ], %args)');
102 3         1465 return;
103             }
104              
105 10 100       25 if(defined($args{'object'})) {
106 8 100       20 if(ref($args{'object'})) {
107 7 100       22 if(ref($args{'object'}) eq __PACKAGE__) {
108 1         14 Carp::carp(__PACKAGE__, ' warning: $object is a cached object');
109             # Note that this isn't a technique for clearing the cache
110 1         376 return $args{'object'};
111             }
112             } else {
113 1         3 Carp::carp(__PACKAGE__, ' $object is a scalar');
114 1         371 return;
115             }
116             } else {
117 2         16 $args{'object'} = Class::Simple->new(%args);
118             }
119              
120             # Warn if we're caching an object that's already cached, then
121             # return the previously cached object. Note that it could be in
122             # a separate cache
123 8         706 my $rc;
124 8 100       57 if($rc = $cached{$args{'object'}}) {
125 2 100       6 unless($args{'quiet'}) {
126 1         4 Carp::carp(__PACKAGE__, ' $object is already cached at ', $rc->{'line'}, ' of ', $rc->{'file'});
127             }
128 2         371 $rc = $rc->{'object'};
129             } else {
130 6         16 $rc = bless \%args, $class;
131 6         23 $cached{$args{'object'}}->{'object'} = $rc;
132 6         31 my @call_details = caller(0);
133 6         98 $cached{$args{'object'}}->{'file'} = $call_details[1];
134 6         24 $cached{$args{'object'}}->{'line'} = $call_details[2];
135             }
136              
137 8         34 return $rc;
138             }
139              
140             =head2 object
141              
142             Return the encapsulated object
143              
144             =cut
145              
146             sub object
147             {
148 1     1 1 3 my $self = shift;
149              
150 1         5 return $self->{'object'};
151             }
152              
153             sub _caller_class
154             {
155 0     0   0 my $self = shift;
156              
157 0 0       0 if(ref($self->{'object'}) eq 'Class::Simple') {
158             # return $self->SUPER::_caller_class(@_);
159 0         0 return $self->Class::Simple::_caller_class(@_);
160             }
161             }
162              
163             =head2 state
164              
165             Returns the state of the object
166              
167             print Data::Dumper->new([$obj->state()]->Dump();
168              
169             =cut
170              
171             sub state {
172 5     5 1 3323 my $self = shift;
173              
174 5         29 return { hits => $self->{_hits}, misses => $self->{_misses} };
175             }
176              
177             # Returns a cached object, if you want it to be uncached, you'll need to clone it
178             sub AUTOLOAD {
179 38     38   10632 our $AUTOLOAD;
180 38         62 my $param = $AUTOLOAD;
181 38         241 $param =~ s/.*:://;
182              
183 38         86 my $self = shift;
184 38         70 my $cache = $self->{'cache'};
185              
186 38 50       90 if($param eq 'DESTROY') {
187 0 0       0 if($cache) {
188 0 0       0 if(ref($cache) eq 'HASH') {
189 0         0 while(my($key, $value) = each %{$cache}) {
  0         0  
190 0         0 delete $cache->{$key};
191             }
192 0         0 return;
193             }
194 0 0 0     0 if(defined($^V) && ($^V ge 'v5.14.0')) {
195 0 0       0 return if ${^GLOBAL_PHASE} eq 'DESTRUCT'; # >= 5.14.0 only
196             }
197 0         0 $cache->clear();
198             }
199 0         0 return;
200             }
201              
202             # my $func = $self->{'object'} . "::$param";
203 38         52 my $func = $param;
204              
205             # if($param =~ /^[gs]et_/) {
206             # # $param = "SUPER::$param";
207             # return $object->$func(\@_);
208             # }
209              
210 38         110 my $key = $param . '::' . join('::', grep defined, @_);
211              
212 38         45 my $rc;
213 38 100       83 if(ref($cache) eq 'HASH') {
214 21         33 $rc = $cache->{$key};
215             } else {
216 17         47 $rc = $cache->get($key);
217             }
218 38 100       992 if(defined($rc)) {
219             # Retrieving a value
220 18 50       39 die $key if($rc eq 'never');
221 18 100       39 if(ref($rc) eq 'ARRAY') {
222 6         12 $self->{_hits}{$key}++;
223 6         10 my @foo = @{$rc};
  6         23  
224 6 100       16 if(wantarray) {
225 5 100       12 if(defined($foo[0])) {
226 4 50       8 die $key if($foo[0] eq __PACKAGE__ . '>UNDEF<');
227 4 50       9 die $key if($foo[0] eq 'never');
228             }
229 5         9 return @{$rc};
  5         18  
230             }
231 1         4 return pop @foo;
232             }
233 12 100       25 if($rc eq __PACKAGE__ . '>UNDEF<') {
234 2         6 $self->{_hits}{$key}++;
235 2         17 return;
236             }
237 10 100       23 if(!wantarray) {
238 9         17 $self->{_hits}{$key}++;
239 9         38 return $rc;
240             }
241             # Want array from cached array after previously requesting it as a scalar
242             }
243 21         47 $self->{_misses}{$key}++;
244 21         32 my $object = $self->{'object'};
245 21 100       39 if(wantarray) {
246 8         70 my @rc = $object->$func(@_);
247 8 100       48 if(scalar(@rc) == 0) {
248 2         6 return;
249             }
250 6 100       18 if(ref($cache) eq 'HASH') {
251 3         7 $cache->{$key} = \@rc;
252             } else {
253 3         8 $cache->set($key, \@rc, 'never');
254             }
255 6         311 return @rc;
256             }
257 13         40 $rc = $object->$func(@_);
258 13 100       61 if(!defined($rc)) {
259 2 100       10 if(ref($cache) eq 'HASH') {
260 1         3 $cache->{$key} = __PACKAGE__ . '>UNDEF<';
261             } else {
262 1         4 $cache->set($key, __PACKAGE__ . '>UNDEF<', 'never');
263             }
264 2         106 return;
265             }
266             # This would be nice, but it does break gedcom. TODO: find out why
267             # if(ref($rc) && (ref($rc) =~ /::/) && (ref($rc) ne __PACKAGE__)) {
268             # $rc = Class::Simple::Readonly::Cached->new(object => $rc, cache => {});
269             # }
270 11 100       29 if(ref($cache) eq 'HASH') {
271 7         30 return $cache->{$key} = $rc;
272             }
273 4         14 return $cache->set($key, $rc, 'never');
274             }
275              
276             =head1 AUTHOR
277              
278             Nigel Horne, C<< <njh at bandsman.co.uk> >>
279              
280             =head1 BUGS
281              
282             Doesn't work with L<Memoize>.
283              
284             Please report any bugs or feature requests to L<https://github.com/nigelhorne/Class-Simple-Readonly-Cached/issues>.
285             I will be notified, and then you'll
286             automatically be notified of progress on your bug as I make changes.
287              
288             =head1 SEE ALSO
289              
290             L<Class::Simple>, L<CHI>
291              
292             =head1 SUPPORT
293              
294             You can find documentation for this module with the perldoc command.
295              
296             perldoc Class::Simple::Readonly::Cached
297              
298             You can also look for information at:
299              
300             =over 4
301              
302             =item * MetaCPAN
303              
304             L<https://metacpan.org/release/Class-Simple-Readonly-Cached>
305              
306             =item * Source Repository
307              
308             L<https://github.com/nigelhorne/Class-Simple-Readonly-Cached>
309              
310             =item * CPANTS
311              
312             L<http://cpants.cpanauthors.org/dist/Class-Simple-Readonly-Cached>
313              
314             =item * CPAN Testers' Matrix
315              
316             L<http://matrix.cpantesters.org/?dist=Class-Simple-Readonly-Cached>
317              
318             =item * CPAN Ratings
319              
320             L<http://cpanratings.perl.org/d/Class-Simple-Readonly-Cached>
321              
322             =item * CPAN Testers Dependencies
323              
324             L<http://deps.cpantesters.org/?module=Class::Simple::Readonly::Cached>
325              
326             =item * Search CPAN
327              
328             L<http://search.cpan.org/dist/Class-Simple-Readonly-Cached/>
329              
330             =back
331              
332             =head1 LICENSE AND COPYRIGHT
333              
334             Author Nigel Horne: C<njh@bandsman.co.uk>
335             Copyright (C) 2019-2023 Nigel Horne
336              
337             Usage is subject to licence terms.
338             The licence terms of this software are as follows:
339             Personal single user, single computer use: GPL2
340             All other users (including Commercial, Charity, Educational, Government)
341             must apply in writing for a licence for use from Nigel Horne at the
342             above e-mail.
343             =cut
344              
345             1;