File Coverage

lib/Class/Simple/Readonly/Cached.pm
Criterion Covered Total %
statement 121 134 90.3
branch 57 74 77.0
condition 13 28 46.4
subroutine 12 12 100.0
pod 5 5 100.0
total 208 253 82.2


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