• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nigelhorne / Class-Simple-Readonly-Cached / 13104313445

03 Feb 2025 01:04AM UTC coverage: 73.013% (-0.03%) from 73.042%
13104313445

push

github

487 of 667 relevant lines covered (73.01%)

7.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.59
/lib/Class/Simple/Readonly/Cached.pm
1
package Class::Simple::Readonly::Cached;
2

3
use strict;
6✔
4
use warnings;
6✔
5

6✔
6
use Carp;
6✔
7
use Class::Simple;
8
use Data::Reuse;
9
use Params::Get;
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.
19✔
83

19✔
84
=cut
85

86
sub new
19✔
87
{
3✔
88
        my $class = shift;
89

1✔
90
        # Use Class::Simple::Readonly::Cached->new(), not Class::Simple::Readonly::Cached::new()
1✔
91
        if(!defined($class)) {
92
                carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
15✔
93
                return;
94
        }
95
        if(Scalar::Util::blessed($class)) {
96
                my $params = Params::Get::get_params(undef, @_) || {};
18✔
97
                # clone the given object
×
98
                return bless { %{$class}, %{$params} }, ref($class);
×
99
        }
100

101
        my $params = Params::Get::get_params('cache', @_);
1✔
102

103
        # Ensure cache implements required methods
104
        if(Scalar::Util::blessed($params->{cache})) {
17✔
105
                if((ref($params->{cache}) ne 'HASH') && !($params->{cache}->can('get') && $params->{cache}->can('set') && $params->{cache}->can('purge'))) {
3✔
106
                        Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods");
3✔
107
                }
108
        } elsif(ref($params->{'cache'}) ne 'HASH') {
109
                Carp::croak("$class: Cache must be ref to HASH or object");
14✔
110
        }
×
111

112
        if(defined($params->{'object'})) {
113
                if(ref($params->{'object'})) {
14✔
114
                        if(ref($params->{'object'}) eq __PACKAGE__) {
11✔
115
                                Carp::carp(__PACKAGE__, ' warning: $object is a cached object');
10✔
116
                                # Note that this isn't a technique for clearing the cache
1✔
117
                                return $params->{'object'};
118
                        }
1✔
119
                } else {
120
                        Carp::carp(__PACKAGE__, ' $object is a scalar');
121
                        return;
1✔
122
                }
1✔
123
        } else {
124
                # FIXME: If there are arguments, put the values in the cache
125

3✔
126
                $params->{'object'} = Class::Simple->new(%{$params});
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
12✔
132
        my $rc;
12✔
133
        if($rc = $cached{$params->{'object'}}) {
3✔
134
                unless($params->{'quiet'}) {
2✔
135
                        Carp::carp(__PACKAGE__, ' $object is already cached at ', $rc->{'line'}, ' of ', $rc->{'file'});
136
                }
3✔
137
                return $rc->{'object'};
138
        }
9✔
139
        $rc = bless $params, $class;
9✔
140
        $cached{$params->{'object'}}->{'object'} = $rc;
9✔
141
        my @call_details = caller(0);
9✔
142
        $cached{$params->{'object'}}->{'file'} = $call_details[1];
9✔
143
        $cached{$params->{'object'}}->{'line'} = $call_details[2];
144

145
        # Return the blessed object
9✔
146
        return $rc;
147
}
148

149
=head2 object
150

151
Return the encapsulated object
152

153
=cut
154

155
sub object
156
{
2✔
157
        my $self = shift;
158

2✔
159
        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
{
8✔
182
        my $self = shift;
183

8✔
184
        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
{
4✔
195
        my ($self, $method) = @_;
4✔
196

197
        return ($method eq 'new') || $self->{'object'}->can($method) || $self->SUPER::can($method);
198
}
4✔
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
        my ($self, $class) = @_;
209

16✔
210
        if($class eq ref($self) || ($class eq __PACKAGE__) || $self->SUPER::isa($class)) {
16✔
211
                return 1;
212
        }
16✔
213
        return $self->{'object'}->isa($class);
13✔
214
}
215

3✔
216

217
# Returns a cached object, if you want it to be uncached, you'll need to clone it
218
sub AUTOLOAD
219
{
220
        our $AUTOLOAD;
221
        my ($param) = $AUTOLOAD =~ /::(\w+)$/;
222

47✔
223
        my $self = shift;
47✔
224
        my $cache = $self->{'cache'};
47✔
225

226
        if($param eq 'DESTROY') {
47✔
227
                if(defined($^V) && ($^V ge 'v5.14.0')) {
47✔
228
                        return if ${^GLOBAL_PHASE} eq 'DESTRUCT';        # >= 5.14.0 only
229
                }
47✔
230
                if($cache) {
1✔
231
                        if(ref($cache) eq 'HASH') {
1✔
232
                                my $class = ref($self);
1✔
233
                                # while(my($key, $value) = each %{$cache}) {
×
234
                                        # if($key =~ /^$class/) {
235
                                                # delete $cache->{$key};
1✔
236
                                        # }
237
                                # }
×
238
                                delete $cache->{$_} for grep { /^$class/ } keys %{$cache};
×
239
                                return;
240
                        }
×
241
                        $cache->purge();
242
                }
×
243
                return;
244
        }
245

246
        # my $method = $self->{'object'} . "::$param";
46✔
247
        my $method = $param;
248

249
        # if($param =~ /^[gs]et_/) {
250
                # # $param = "SUPER::$param";
251
                # return $object->$method(\@_);
252
        # }
253

46✔
254
        my $key = ref($self) . "::${param}::" . join('::', grep defined, @_);
255

46✔
256
        my $rc;
46✔
257
        if(ref($cache) eq 'HASH') {
28✔
258
                $rc = $cache->{$key};
259
        } else {
18✔
260
                $rc = $cache->get($key);
261
        }
46✔
262
        if(defined($rc)) {
263
                # Retrieving a value
22✔
264
                die $key if($rc eq 'never');
22✔
265
                if(ref($rc) eq 'ARRAY') {
6✔
266
                        $self->{_hits}{$key}++;
6✔
267
                        my @foo = @{$rc};
6✔
268
                        if(wantarray) {
5✔
269
                                if(defined($foo[0])) {
4✔
270
                                        die $key if($foo[0] eq __PACKAGE__ . '>UNDEF<');
4✔
271
                                        die $key if($foo[0] eq 'never');
272
                                }
273
                                # return @{$rc};
5✔
274
                                return @foo;
275
                        }
1✔
276
                        return pop @foo;
277
                }
16✔
278
                if($rc eq __PACKAGE__ . '>UNDEF<') {
2✔
279
                        $self->{_hits}{$key}++;
2✔
280
                        return;
281
                }
14✔
282
                if(!wantarray) {
13✔
283
                        $self->{_hits}{$key}++;
13✔
284
                        return $rc;
285
                }
286
                # Want array from cached array after previously requesting it as a scalar
287
        }
25✔
288
        $self->{_misses}{$key}++;
25✔
289
        my $object = $self->{'object'};
25✔
290
        if(wantarray) {
8✔
291
                my @rc = $object->$method(@_);
8✔
292
                if(scalar(@rc) == 0) {
2✔
293
                        if(ref($cache) eq 'HASH') {
294
                                $cache->{$key} = __PACKAGE__ . '>UNDEF<';
6✔
295
                        } else {
3✔
296
                                $cache->set($key, __PACKAGE__ . '>UNDEF<', 'never');
297
                        }
3✔
298
                        return;
299
                }
6✔
300
                my $can_fixate = 1;        # Work around for RT#163955
301
                foreach (@rc) {
17✔
302
                        if(ref($_)) {
17✔
303
                                if(ref($_) eq 'GLOB') {
2✔
304
                                        $can_fixate = 0;
1✔
305
                                        last;
306
                                }
1✔
307
                                if((ref($_) ne 'ARRAY') && (ref($_) ne 'HASH') && (ref($_) ne 'SCALAR')) {
308
                                        $can_fixate = 0;
2✔
309
                                        last;
310
                                }
311
                        }
312
                }
313
                Data::Reuse::fixate(@rc) if($can_fixate);
314
                if(ref($cache) eq 'HASH') {
315
                        $cache->{$key} = \@rc;
15✔
316
                } else {
10✔
317
                        $cache->set($key, \@rc, 'never');
318
                }
5✔
319
                return @rc;
320
        }
321
        $rc = $object->$method(@_);
322
        if(!defined($rc)) {
323
                if(ref($cache) eq 'HASH') {
324
                        $cache->{$key} = __PACKAGE__ . '>UNDEF<';
325
                } else {
326
                        $cache->set($key, __PACKAGE__ . '>UNDEF<', 'never');
327
                }
328
                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
        if(ref($cache) eq 'HASH') {
336
                return $cache->{$key} = $rc;
337
        }
338
        return $cache->set($key, $rc, 'never');
339
}
340

341
=head1 AUTHOR
342

343
Nigel Horne, C<< <njh at nigelhorne.com> >>
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;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc