File Coverage

blib/lib/Plack/Middleware/Cached.pm
Criterion Covered Total %
statement 77 77 100.0
branch 27 34 79.4
condition 10 15 66.6
subroutine 16 16 100.0
pod 2 5 40.0
total 132 147 89.8


line stmt bran cond sub pod time code
1 5     5   180100 use strict;
  5         15  
  5         162  
2 5     5   29 use warnings;
  5         8  
  5         179  
3             package Plack::Middleware::Cached;
4              
5 5     5   689 use parent 'Plack::Middleware';
  5         434  
  5         25  
6 5     5   14429 use Scalar::Util qw(blessed reftype);
  5         10  
  5         452  
7 5     5   23 use Carp 'croak';
  5         9  
  5         247  
8 5     5   36 use Plack::Util::Accessor qw(cache key set env);
  5         8  
  5         30  
9 5     5   4767 use utf8;
  5         68  
  5         21  
10              
11             our $VERSION = '0.15';
12              
13             sub prepare_app {
14 13     13 1 29881 my ($self) = @_;
15              
16 13 50       51 croak "expected cache" unless $self->cache;
17              
18 13 50       450 croak "cache object must provide get and set"
19             unless can_cache( $self->cache );
20              
21             # define how the caching key is calculated
22 13 100 66     44 if (ref $self->key and ref $self->key eq 'ARRAY') {
    100          
23 1         28 my $key = $self->key;
24             $self->key( sub {
25 5     5   49 my $env = shift;
26             # stringify subset of the request environment
27 7         26 join '\1E',
28 10         20 map { ($_, $env->{$_}) }
29 5         61 grep { defined $env->{$_} } @$key;
30 1         10 } );
31             } elsif (not $self->key) {
32 11         143 $self->key('REQUEST_URI');
33             }
34              
35 13 100   15   121 $self->set( sub { $_[0] } ) unless $self->set;
  15         193  
36             }
37              
38             sub call {
39 29     29 1 44197 my ($self, $env) = @_;
40              
41 29 100       82 my $key = ref($self->key) ? $self->key->($env) : $env->{$self->key};
42              
43 29 50       290 return $self->app_code->($env) unless defined $key;
44              
45             # get from cache
46 29         79 my $object = $self->cache->get( $key );
47 29 100       234 if (defined $object) {
48 11         16 my ($response, $mod_env) = @{$object};
  11         19  
49 11 100       30 if ($mod_env) { # TODO: should we check $self->env ?
50 1         7 while ( my ($key, $value) = each %$mod_env ) {
51 1         5 $env->{$key} = $value;
52             }
53             }
54 11         23 $env->{'plack.middleware.cached'} = 1;
55 11         37 return $response;
56             }
57              
58             # pass through and cache afterwards
59 18         48 my $response = $self->app_code->($env);
60              
61             # streaming response
62 18 100       177 if (ref $response eq 'CODE') {
63             $response = $self->response_cb($response, sub {
64 2     2   54 my ($ret) = @_;
65 2         2 my $seen;
66 2         23 my $body = '';
67             return sub {
68 5         172 my ($chunk) = @_;
69 5 100 100     22 if ($seen++ and not defined $chunk) {
70 2         6 my $new_response = [ $ret->[0], $ret->[1], [ $body ] ];
71 2         6 $self->cache_response($key, $new_response, $env);
72 2         91 return;
73             }
74 3 50       8 $body .= $chunk if defined $chunk;
75 3         16 return $chunk;
76 2         11 };
77 2         20 });
78             } else {
79 16         45 $self->cache_response($key, $response, $env);
80             }
81              
82 18         416 return $response;
83             }
84              
85             # cache a response based on configuration of this middleware
86             sub cache_response {
87 18     18 0 34 my ($self, $key, $response, $env) = @_;
88              
89 18         53 my @options = $self->set->($response, $env);
90 18 50 66     110 if (@options and $options[0]) {
91 16         28 $options[0] = [ $options[0] ];
92 16         48 my $env_vars = $self->env;
93 16 100       84 if ($env_vars) {
94 1 50       4 $env_vars = [$env_vars] unless ref $env_vars;
95 1         6 $options[0]->[1] = {
96 1         3 map { $_ => $env->{$_} } @$env_vars
97             };
98             }
99 16         42 $self->cache->set( $key, @options );
100             }
101             }
102              
103             # allows caching PSGI-like applications not derived from Plack::Component
104             sub app_code {
105 18     18 0 72 my $app = shift->app;
106              
107             (blessed $app and $app->can('call'))
108 1     1   5 ? sub { $app->call(@_) }
109 18 100 66     177 : $app;
110             }
111              
112             # duck typing test
113             sub can_cache {
114 13     13 0 66 my $cache = shift;
115              
116 13 50 33     272 blessed $cache and
117             $cache->can('set') and $cache->can('get');
118             }
119              
120             1;
121              
122             =head1 NAME
123              
124             Plack::Middleware::Cached - Glues a cache to your PSGI application
125              
126             =head1 SYNOPSIS
127              
128             use Plack::Builder;
129             use Plack::Middleware::Cached;
130              
131             my $cache = CHI->new( ... ); # create a cache
132              
133             builder {
134             enable 'Cached', # enable caching
135             cache => $cache, # using this cache
136             key => 'REQUEST_URI', # using this key from env
137             env => ['my.a','my.b']; # and cache $env{'my.a'} and $env{'my.b'},
138             $app;
139             }
140              
141             # alternative creation without Plack::Builder
142             Plack::Middleware::Cached->wrap( $app, cache => $cache );
143              
144             =head1 DESCRIPTION
145              
146             This module can be used to glue a cache to a L applications or
147             middleware. A B is an object that provides at least two methods to get
148             and set data, based on a key. Existing cache modules on CPAN include L,
149             L, and L. Although this module aims at caching PSGI
150             applications, you can use it to cache any function that returns some response
151             object based on a request environment.
152              
153             Plack::Middleware::Cached is put in front of a PSGI application as middleware.
154             Given a request in form of a PSGI environment E, it either returns the matching
155             response R from its cache, or it passed the request to the wrapped application,
156             and stores the application's response in the cache:
157              
158             ________ _____
159             Request ===E===>| |---E--->| |
160             | Cached | | App |
161             Response <==R====|________|<--R----|_____|
162              
163             In most cases, only a part of the environment E is relevant to the request.
164             This relevant part is called the caching B. By default, the key is set
165             to the value of REQUEST_URI from the environment E.
166              
167             Some application may also modify the environment E:
168              
169             ________ _____
170             Request ===E===>| |---E--->| |
171             | Cached | | App |
172             Response <==R+E==|________|<--R+E--|_____|
173              
174             If needed, you can configure Plack::Middleware::Cached with B to also
175             cache parts of the environment E, as it was returned by the application.
176              
177             If Plack::Middleware::Cached retrieved a response from the cache, it sets the
178             environment variable C. You can inspect whether a
179             response came from the cache or from the wrapped application like this:
180              
181             builder {
182             enable sub {
183             my $app = shift;
184             sub {
185             my $env = shift;
186             my $res = $app->($env);
187             if ($env->{'plack.middleware.cached') {
188             ...
189             }
190             return $res;
191             };
192             };
193             enable 'Cached', cache => $cache;
194             $app;
195             },
196              
197             Caching delayed/streaming responses is supported as well.
198              
199             =head1 CONFIGURATION
200              
201             =over 4
202              
203             =item cache
204              
205             An cache object, which supports the methods C<< get( $key ) >> to retrieve
206             an object from cache and C<< set( $key, $object [, @options ] ) >> to store
207             an object in cache, possibly adjusted by some options. See L for a class
208             than can be used to create cache objects.
209              
210             =item key
211              
212             Key to map a PSGI environment to a scalar key. By default only the REQUEST_URI
213             variable is used, but you can provide another variable as scalar, a combination
214             of variables as array reference, or a code reference that is called to
215             calculate a key, given a PSGI environment. If this code returns undef, the
216             request is not cached.
217              
218             =item env
219              
220             Name of an environment variable or array reference with multiple variables from
221             the environment that should be cached together with a response.
222              
223             =item set
224              
225             Code reference to determine a policy for storing data in the cache. Each time
226             a response (and possibly environment data) is to be stored in the cache, it
227             is passed to this function. The code is expected to return an array with the
228             response as first value and optional options to the cache's 'set' method as
229             additional values. For instance you can pass an expiration time like this:
230              
231             set => sub {
232             my ($response, $env) = @_;
233             return ($response, expires_in => '20 min');
234             }
235              
236             You can also use this method to skip selected responses from caching:
237              
238             set => sub {
239             my ($response, $env) = @_;
240             if ( $some_condition_not_to_cache_this_response ) {
241             return;
242             }
243             return $response;
244             }
245              
246             =back
247              
248             =head1 SEE ALSO
249              
250             There already are several modules for caching PSGI applications:
251             L by Ingy döt Net implements a simple file
252             cache for PSGI responses. Panu Ervamaa created a more general module of
253             same name, available at L.
254              
255             =encoding utf8
256              
257             =head1 AUTHOR
258            
259             Jakob Voß
260            
261             =head1 COPYRIGHT AND LICENSE
262            
263             This software is copyright (c) 2013 by Jakob Voß.
264            
265             This is free software; you can redistribute it and/or modify it under
266             the same terms as the Perl 5 programming language system itself.
267            
268             =cut