File Coverage

File:blib/lib/Class/Simple/Cached.pm
Coverage:95.1%

linestmtbrancondsubtimecode
1package Class::Simple::Cached;
2
3
13
13
13
563617
8
56
use strict;
4
13
13
13
10
7
118
use warnings;
5
6
13
13
13
12
8
33
use Carp ();
7
13
13
13
1477
14656
112
use Class::Simple;
8
13
13
13
1088
75204
125
use Params::Get 0.15;
9
13
13
13
17
5
42
use Scalar::Util ();
10
13
13
13
1738
86214
24
use Sub::Protected;
11
12# Stored in the cache to distinguish "the object returned undef" from
13# "this key has never been cached".  Must never be a legitimate return value.
14
13
13
13
701
7
206
use constant UNDEF_SENTINEL => __PACKAGE__ . '>UNDEF<';
15
16# Evaluated once at compile time so DESTROY never pays a string comparison
17# on every object teardown just to decide whether ${^GLOBAL_PHASE} exists.
18
13
13
13
8
7
2664
use constant _GLOBAL_PHASE_AVAILABLE => defined($^V) && ($^V ge 'v5.14.0');
19
20 - 28
=head1 NAME

Class::Simple::Cached - cache getter results for any get/set object

=head1 VERSION

Version 0.07

=cut
29
30our $VERSION = '0.07';
31
32=encoding UTF-8
33
34 - 149
=head1 SYNOPSIS

    use CHI;
    use Class::Simple::Cached;

    # Wrap an existing object with a cache layer
    my $cache = CHI->new(driver => 'RawMemory', global => 1);
    my $obj   = Class::Simple::Cached->new(
        cache  => $cache,
        object => My::Expensive::Object->new(),
    );

    $obj->name('Alice');      # setter: delegates to wrapped object, updates cache
    my $n = $obj->name();     # getter: returns cached 'Alice' without hitting object

    # Or use a plain hash ref as the cache backend
    my %store;
    my $simple = Class::Simple::Cached->new(cache => \%store);
    $simple->colour('blue');
    print $simple->colour();  # 'blue', served from %store

=head1 DESCRIPTION

A transparent caching wrapper for any L<Class::Simple>-compatible get/set object.
Repeated getter calls hit the cache instead of the wrapped object, avoiding
expensive recomputation or remote round-trips.

Cache coherency is I<not> automatic.  If the wrapped object's state changes
through a path other than the cached wrapper, callers must invalidate the cache
themselves.

=head1 SUBROUTINES/METHODS

=head2 new

Constructs a C<Class::Simple::Cached> instance that wraps C<object> behind
C<cache>.

=head3 ARGUMENTS

=over 4

=item C<cache> (mandatory)

Either a blessed object implementing C<get($key)>, C<set($key, $val, $expires)>,
and C<purge()> (e.g. any L<CHI> driver); or a plain hash reference used as an
in-process store.

=item C<object> (optional)

The object whose methods will be proxied and cached.  Defaults to a fresh
C<Class::Simple> instance.

=back

Calling C<< ->new() >> on an already-blessed instance returns a shallow clone
(all stored fields are merged, including the existing cache handle).

=head3 RETURNS

A blessed C<Class::Simple::Cached> instance.

=head3 SIDE EFFECTS

None beyond allocating the new instance.

=head3 EXAMPLE

    # CHI-backed cache
    use CHI;
    my $obj = Class::Simple::Cached->new(
        cache  => CHI->new(driver => 'RawMemory', global => 1),
        object => My::Model->new(),
    );

    # Hash-ref cache (useful for tests or short-lived objects)
    my $obj = Class::Simple::Cached->new(cache => {});

    # Clone an existing wrapped object
    my $clone = $obj->new();

=head3 API SPECIFICATION

    Input:
      cache  => HashRef | CHICompatibleObject  # required
      object => Object                          # optional

    Output:
      Class::Simple::Cached instance

=head3 MESSAGES

    Message                                              Meaning                              Resolution
    ---------------------------------------------------  -----------------------------------  ----------------------------------------
    "use ->new() not ::new() to instantiate"             Called as Class::Simple::Cached::new  Use $obj->new() or ClassName->new()
    "Usage: $class->new(cache => \$cache)"               No arguments supplied                 Pass at least cache => ...
    "Cache must be ref to HASH or object"                cache is a plain scalar or wrong ref  Use a hashref or CHI-compatible object
    "Cache object must implement get, set, purge"        Blessed cache lacks required methods  Use a fully CHI-compatible object

=head3 PSEUDOCODE

    new(class, args):
      IF class undefined   â†’ carp and return undef
      IF class is blessed  â†’ merge fields, return shallow clone
      IF no args           â†’ croak Usage message
      PARSE args into hashref via Params::Get
      IF params.object absent → params.object = Class::Simple->new()
      IF params.cache is a blessed object:
        VERIFY it can('get') AND can('set') AND can('purge')
        IF not → croak capability message
        RETURN bless params, class
      IF params.cache is a HASH ref:
        RETURN bless params, class
      croak "Cache must be ref to HASH or object"

=cut
150
151sub new
152{
153
218
663948
        my $class = shift;
154
155        # Guard: always call as a method, not a bare function
156
218
148
        if(!defined($class)) {
157
3
5
                Carp::carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
158
3
241
                return;
159        }
160
161        # When called on a blessed instance, return a shallow clone
162
215
148
        if(Scalar::Util::blessed($class)) {
163
16
18
                my $params = Params::Get::get_params(undef, \@_) || {};
164
16
16
16
107
11
16
                my %merged = (%{$class}, %{$params});
165                # Always recalculate internal dispatch fields after the merge so that
166                # caller-supplied _is_hash_cache or _cache_prefix args cannot corrupt
167                # the cache-type flag or the key-prefix (internal field injection).
168
16
15
                $merged{'_is_hash_cache'} = ref($merged{'cache'}) eq 'HASH';
169
16
13
                $merged{'_cache_prefix'}  = ref($class) . ':';
170
171                # Validate the (potentially-replaced) cache object using the same rules
172                # as new() — the clone path must not produce an unusable object.
173
16
11
                if(!$merged{'_is_hash_cache'}) {
174
3
4
                        if(Scalar::Util::blessed($merged{'cache'})) {
175
1
3
                                unless($merged{'cache'}->can('get')
176                                        && $merged{'cache'}->can('set')
177                                        && $merged{'cache'}->can('purge'))
178                                {
179
1
2
                                        Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods");
180                                }
181                        } else {
182
2
4
                                Carp::croak(ref($class) . ': Cache must be ref to HASH or object');
183                        }
184                }
185
186
13
17
                return bless \%merged, ref($class);
187        }
188
189        # Require at least one argument so Params::Get's confess is never reached;
190        # we want croak (Test::Carp-compatible) not confess.
191
199
112
        if(scalar(@_) == 0) {
192
5
25
                Carp::croak('Usage: ', $class, '->new(cache => $cache)');
193        }
194
195
194
153
        my $params = Params::Get::get_params('cache', @_) || {};
196
197        # Default the wrapped object to a bare Class::Simple instance
198
194
1465
        $params->{'object'} ||= Class::Simple->new();
199
200        # Precompute per-instance fields so AUTOLOAD (hot path) avoids ref() and
201        # string concatenation on every call.
202
194
3204
        $params->{'_is_hash_cache'} = ref($params->{'cache'}) eq 'HASH';
203
194
114
        $params->{'_cache_prefix'}  = "$class:";
204
205
194
134
        if(Scalar::Util::blessed($params->{'cache'})) {
206                # Verify the cache object speaks the required interface
207
36
115
                unless($params->{'cache'}->can('get')
208                        && $params->{'cache'}->can('set')
209                        && $params->{'cache'}->can('purge'))
210                {
211
14
31
                        Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods");
212                }
213
22
38
                return bless $params, $class;
214        }
215
216        # Transitive reduction: _is_hash_cache already holds ref($cache) eq 'HASH'
217        # computed two statements earlier — no need to call ref() again.
218
158
107
        if($params->{'_is_hash_cache'}) {
219
130
80
                return bless $params, $class;
220        }
221
222
28
60
        Carp::croak("$class: Cache must be ref to HASH or object");
223}
224
225 - 254
=head2 can

Reports whether this wrapper (or its embedded object) can handle a method.

=head3 ARGUMENTS

=over 4

=item C<$method> — the method name to probe.

=back

=head3 RETURNS

True if the method is known; false otherwise.

=head3 EXAMPLE

    $obj->can('name');   # true if the wrapped object has a name() method

=head3 API SPECIFICATION

    Input:  method_name : Str
    Output: Bool

=head3 MESSAGES

    None.

=cut
255
256sub can
257{
258
47
1438
        my ($self, $method) = @_;
259
260        # Undefined method name is outside the documented API; return undef cleanly.
261
47
37
        return unless defined $method;
262
263        # When called as a class method there is no wrapped object to probe
264
46
76
        return $self->SUPER::can($method) unless Scalar::Util::blessed($self);
265
266        return ($method eq 'new')
267
16
47
                || $self->{'object'}->can($method)
268                || $self->SUPER::can($method);
269}
270
271 - 300
=head2 isa

Reports whether this wrapper or its embedded object is of a given class.

=head3 ARGUMENTS

=over 4

=item C<$class> — the class name to test.

=back

=head3 RETURNS

True if the wrapper or the wrapped object is-a C<$class>.

=head3 EXAMPLE

    $obj->isa('My::Model');   # delegates to the wrapped object

=head3 API SPECIFICATION

    Input:  class_name : Str
    Output: Bool

=head3 MESSAGES

    None.

=cut
301
302sub isa
303{
304
39
1619
        my ($self, $class) = @_;
305
306        # Undefined class name is outside the documented API; return undef cleanly.
307
39
20
        return unless defined $class;
308
309        # When called as a class method there is no wrapped object to interrogate
310
38
44
        return $self->SUPER::isa($class) unless Scalar::Util::blessed($self);
311
312
35
64
        return 1 if $class eq ref($self)
313                || $class eq __PACKAGE__
314                || $self->SUPER::isa($class);
315
316
11
20
        return $self->{'object'}->isa($class);
317}
318
319# DESTROY — purge this instance's entries from the cache on object teardown.
320#
321# Purpose:      Prevent stale entries from leaking into a shared cache after
322#               the wrapper goes out of scope.
323# Entry:        Called automatically by Perl's garbage collector.
324# Exit Status:  None (void).
325# Side Effects: Removes all cache keys prefixed with ref($self) for hash caches;
326#               calls purge() for CHI-style caches.
327#               See https://github.com/Perl/perl5/issues/14673 for why an
328#               explicit DESTROY is required even though AUTOLOAD could catch it.
329sub DESTROY
330{
331
168
39662
        my $self = shift;
332
333
168
120
        my $cache = $self->{'cache'} or return;
334
335
165
104
        if($self->{'_is_hash_cache'}) {
336                # Remove only this class's keys to avoid stomping on siblings sharing the hash
337
143
62
                my $prefix = $self->{'_cache_prefix'};
338
143
605
143
63
325
107
                delete $cache->{$_} for grep { index($_, $prefix) == 0 } keys %{$cache};
339
143
199
                return;
340        }
341
342        # Skip purge during global destruction to avoid order-of-destruction crashes
343
22
20
        return if _GLOBAL_PHASE_AVAILABLE && ${^GLOBAL_PHASE} eq 'DESTRUCT';
344
345
22
21
        $cache->purge();
346}
347
348# _cache_get / _cache_set — unified read/write; hides hash-ref vs. CHI dispatch
349# so the rest of the code never needs to branch on ref($cache).
350sub _cache_get :Protected
351{
352        my ($self, $key) = @_;
353        my $cache = $self->{'cache'};
354        return $self->{'_is_hash_cache'} ? $cache->{$key} : $cache->get($key);
355
13
13
13
32
1
101
}
356
357sub _cache_set :Protected
358{
359        my ($self, $key, $val) = @_;
360        my $cache = $self->{'cache'};
361        if($self->{'_is_hash_cache'}) {
362                $cache->{$key} = $val;
363        } else {
364                $cache->set($key, $val, 'never');
365        }
366
13
13
13
1014
4
52
}
367
368 - 458
=head2 AUTOLOAD (getter/setter proxy)

Intercepts every method call that is not explicitly defined, proxying it to
the wrapped object with a caching layer for zero-argument (getter) calls.

Setter calls (one or more arguments) always pass through to the wrapped object
and update the cache with the new value.

=head3 ARGUMENTS

    $method()        â€” getter: returns cached value if present, else calls object
    $method($scalar) — scalar setter: stores scalar, updates cache
    $method(@list)   â€” array setter: stores list, updates cache

=head3 RETURNS

The value returned by the wrapped object (or the cached copy thereof).

=head3 SIDE EFFECTS

=over 4

=item * Getter: may write to the cache on first call.

=item * Setter: writes to both the wrapped object and the cache.

=item * DESTROY: clears cache entries for this instance (see above).

=back

=head3 EXAMPLE

    $obj->colour('red');    # setter — writes 'red' to object and cache
    $obj->colour();         # getter — returns 'red' from cache

=head3 API SPECIFICATION

    Input (getter):  method_name : Str,  args : ()
    Input (setter):  method_name : Str,  args : (Scalar | List)
    Output:          the stored/retrieved value or list

=head3 MESSAGES

    Message                           Meaning                             Resolution
    --------------------------------  ----------------------------------  ----------------------------------------
    "$method" (croak)                 Cached array's first element is     Do not store the sentinel string
                                      the UNDEF_SENTINEL string           as a real value in the wrapped object

=head3 PSEUDOCODE

    AUTOLOAD(method, args...):
      key = ref(self) + ":" + method

      IF no args (getter mode):
        val = cache_get(key)
        IF cache hit:
          IF val is a plain string (not a ref):
            IF val is UNDEF_SENTINEL → return undef
            RETURN val
          IF val is an arrayref:
            IF first element is a plain string AND equals UNDEF_SENTINEL → croak
            RETURN dereferenced list
          RETURN val (blessed object)
        # Cache miss — ask the wrapped object
        IF list context:
          result_list = object->method()
          IF empty      â†’ return ()
          cache_set(key, \result_list)
          RETURN result_list
        # Scalar context
        result = object->method()
        IF defined:
          cache_set(key, result)
          RETURN result
        cache_set(key, UNDEF_SENTINEL)
        RETURN undef

      ELSE (setter mode):
        IF more than one arg (array setter):
          val = object->method(\@args)     # wrapped object stores arrayref
          IF defined:
            cache_set(key, val)
            RETURN @val
          cache_set(key, UNDEF_SENTINEL)
          RETURN undef
        ELSE (scalar setter):
          val = object->method(args[0])
          cache_set(key, val // UNDEF_SENTINEL)
          RETURN val

=cut
459
460sub AUTOLOAD
461{
462
735
14390
        our $AUTOLOAD;
463        # rindex+substr avoids regex engine startup on every method dispatch
464
735
341
        my $param = substr($AUTOLOAD, rindex($AUTOLOAD, '::') + 2);
465
466
735
274
        my $self = shift;
467        # _cache_prefix is precomputed at new() time (= ref($self) . ":") to avoid
468        # calling ref() and doing string concat on every AUTOLOAD invocation.
469
735
295
        my $key  = $self->{'_cache_prefix'} . $param;
470
735
267
        my $object = $self->{'object'};
471
472        # Getter path ─────────────────────────────────────────────────────────────
473
735
339
        if(scalar(@_) == 0) {
474
155
95
                my $rc = $self->_cache_get($key);
475
476                # Truthiness check: falsy-but-defined values (0, '') are treated as cache
477                # misses so the object is re-invoked every call.  This is a known
478                # limitation; see the LIMITATIONS section in the POD.
479
154
525
                if($rc) {
480                        # Premise 1: $rc is truthy (the if-guard guarantees this).
481                        # Premise 2: ref($rc) returns '' for plain scalars, 'ARRAY' for
482                        #   arrayrefs, or a package name for blessed objects.
483                        # The three branches below are mutually exclusive and exhaustive.
484
103
70
                        if(!ref($rc)) {
485                                # Conclusion: plain scalar — only type that can equal the sentinel.
486
80
57
                                return if $rc eq UNDEF_SENTINEL;
487
68
78
                                return $rc;
488                        } elsif(ref($rc) eq 'ARRAY') {
489                                # Premise: !ref($rc) was false, so ref($rc) is truthy; no need
490                                # to re-test ref() — the elsif is the deductive next step.
491
18
44
                                Carp::croak($param)
492                                        if !ref($rc->[0]) && $rc->[0] eq UNDEF_SENTINEL;
493
14
14
6
20
                                return @{$rc};
494                        }
495                        # Conclusion: ref($rc) is truthy and ≠ 'ARRAY' → blessed object.
496                        # Overloaded eq was never invoked; return the object as-is.
497
5
5
                        return $rc;
498                }
499
500                # Cache miss — call the real object
501
51
35
                if(wantarray) {
502
19
16
                        my @result = $object->$param();
503
19
49
                        return unless scalar(@result);  # empty list: don't cache, return ()
504
10
15
                        $self->_cache_set($key, \@result);
505
10
159
                        return @result;
506                }
507
508                # Scalar / void context: cache the value (or the sentinel for undef)
509
32
30
                my $val = $object->$param();
510
31
154
                $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
511
30
176
                return $val;
512        }
513
514        # Setter path — array ─────────────────────────────────────────────────────
515
580
242
        if(scalar(@_) > 1) {
516                # Pass the list as an arrayref; the wrapped object stores it
517
12
18
                my $val = $object->$param(\@_);
518
12
123
                $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
519
12
10
108
15
                return defined($val) ? @{$val} : ();
520        }
521
522        # Setter path — scalar ────────────────────────────────────────────────────
523        # CHI's set() returns the cache object, not the stored value;
524        # capture the value before the set call and return it directly.
525
568
540
        my $val = $object->$param($_[0]);
526
567
1467
        $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
527
566
410
        return $val;
528}
529
530 - 689
=head1 FORMAL SPECIFICATION

=head2 new

    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    [State]
      cache  : â„™(HashRef ∪ CHIObject)
      object : Object

    [CHIObject]
      can_get   : Method
      can_set   : Method
      can_purge : Method

    new ──────────────────────────────────────────────────────────────
    Î”(cache, object)
    cache? : HashRef ∪ CHIObject
    object? : Object ∪ {∅}
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    cache? ≠ ∅
    cache ′ = cache?
    object ′ = (object? ≠ ∅ ⟹ object?) ∨ Class::Simple.new()
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

=head2 can

    can ──────────────────────────────────────────────────
    Îž(cache, object)
    method? : MethodName
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    result! = (method? = 'new')
            âˆ¨ object.can(method?)
            âˆ¨ SUPER::can(method?)

=head2 isa

    isa ──────────────────────────────────────────────────
    Îž(cache, object)
    class? : ClassName
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    result! = (class? = ref(self))
            âˆ¨ (class? = __PACKAGE__)
            âˆ¨ SUPER::isa(class?)
            âˆ¨ object.isa(class?)

=head2 AUTOLOAD

    AUTOLOAD ─────────────────────────────────────────────────────────
    Î”(cache)
    method? : MethodName
    args?   : Seq(Any)
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    key = ref(self) ⊕ ":" ⊕ method?

    Getter (args? = ∅):
      (∃ v • cache_hit(key, v) ∧ v ≠ UNDEF_SENTINEL ⟹ result! = v)
      âˆ¨ (cache_hit(key, UNDEF_SENTINEL)              âŸ¹ result! = undef)
      âˆ¨ (¬cache_hit(key, _)
          âˆ§ result! = object.method?()
          âˆ§ cache′ = cache ∪ {key ↦ encode(result!)})

    Setter (args? ≠ ∅):
      object′.method?(args?) = args?
      cache′ = cache ∪ {key ↦ encode(object′.method?())}
      result! = args?

=head1 LIMITATIONS

=over 4

=item Setter arguments are not part of the cache key.

Calls like C<< $obj->method($arg) >> use the key C<ClassName:method> regardless
of C<$arg>.  Setters with different arguments therefore overwrite each other's
cache entries.  For read-only caching of parameterised methods, see
L<Class::Simple::Readonly::Cached>.

=item Falsy scalar return values are not cached.

Methods that return C<0> or C<''> (false but defined) are treated as a cache
miss on every call: the underlying object is re-invoked each time.  Only
C<undef> is cached (via the UNDEF_SENTINEL).  If you need to cache C<0>, wrap
it in a container object or use a different caching strategy.

=item Sentinel collision.

If the wrapped object ever returns the literal string
C<< "Class::Simple::Cached\>UNDEF\<" >> as a scalar, or as the first element of a
list, the module will misinterpret it as a cached-undef marker.  This string is
deliberately unusual, but callers working with arbitrary string data should be
aware of the constraint.

=item Shared cache and multiple classes.

When two C<Class::Simple::Cached> instances of B<different> classes share the
same cache object, their keys are namespaced by class name and do not collide.
However, C<purge()> on a CHI-style cache is global: destroying one instance will
purge I<all> entries from a shared CHI cache, including those of the other
instance.  Use per-instance cache objects, or a hash-ref cache, to avoid this.

=item Does not work with L<Memoize>.

=item Overloaded C<eq> on cached values.

If the wrapped object returns a blessed value that overloads the C<eq> operator,
the sentinel comparison in the getter uses a C<ref()> pre-check so that only
plain strings are compared against the sentinel.  Array elements are subject to
the same pre-check.  Callers wrapping objects that return sentinel-like strings
via overloading should be aware of this guard.

=back

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=head1 BUGS

Please report bugs and feature requests at
L<https://github.com/nigelhorne/Class-Simple-Cached/issues>.

=head1 SEE ALSO

L<Class::Simple>, L<CHI>, L<Class::Simple::Readonly::Cached>

=over 4

=item * L<Test Dashboard|https://nigelhorne.github.io/Class-Simple-Cached/coverage/>

=back

=head1 SUPPORT

This module is provided as-is without any warranty.

You can find documentation for this module with the perldoc command:

    perldoc Class::Simple::Cached

=over 4

=item * MetaCPAN: L<https://metacpan.org/release/Class-Simple-Cached>

=item * Source: L<https://github.com/nigelhorne/Class-Simple-Cached>

=item * CPANTS: L<http://cpants.cpanauthors.org/dist/Class-Simple-Cached>

=item * Testers Matrix: L<http://matrix.cpantesters.org/?dist=Class-Simple-Cached>

=back

=head1 LICENCE AND COPYRIGHT

Copyright (C) 2019-2026, Nigel Horne

Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.

=cut
690
6911;