File Coverage

blib/lib/Dancer/Session/DBIC.pm
Criterion Covered Total %
statement 66 93 70.9
branch 12 30 40.0
condition 3 6 50.0
subroutine 16 18 88.8
pod 4 4 100.0
total 101 151 66.8


line stmt bran cond sub pod time code
1             package Dancer::Session::DBIC;
2              
3             =head1 NAME
4              
5             Dancer::Session::DBIC - DBIx::Class session engine for Dancer
6              
7             =head1 VERSION
8              
9             0.005
10              
11             =head1 DESCRIPTION
12              
13             This module implements a session engine for Dancer by serializing the session,
14             and storing it in a database via L. The default serialization method is L,
15             though one can specify any serialization format you want. L and L are
16             viable alternatives.
17              
18             JSON was chosen as the default serialization format, as it is fast, terse, and portable.
19              
20             =head1 SYNOPSIS
21              
22             Example configuration:
23              
24             session: "DBIC"
25             session_options:
26             dsn: "DBI:mysql:database=testing;host=127.0.0.1;port=3306" # DBI Data Source Name
27             schema_class: "Interchange6::Schema" # DBIx::Class schema
28             user: "user" # Username used to connect to the database
29             pass: "password" # Password to connect to the database
30             resultset: "MySession" # DBIx::Class resultset, defaults to Session
31             id_column: "my_session_id" # defaults to sessions_id
32             data_column: "my_session_data" # defaults to session_data
33              
34             In conjunction with L, you can simply use the schema
35             object provided by this plugin in your application, either by
36             providing the name of the schema used by the plugin in the config:
37              
38             session_options:
39             schema: default
40              
41             Or by passing the schema object directly in the code:
42              
43             set session_options => {schema => schema};
44              
45             Custom serializer / deserializer can be specified as follows:
46              
47             set 'session_options' => {
48             schema => schema,
49             serializer => sub { YAML::Dump(@_); },
50             deserializer => sub { YAML::Load(@_); },
51             };
52              
53             =head1 SESSION EXPIRATION
54              
55             A timestamp field that updates when a session is updated is recommended, so you can expire sessions server-side as well as client-side.
56              
57             This session engine will not automagically remove expired sessions on the server, but with a timestamp field as above, you should be able to to do this manually.
58              
59             =head1 RESULT CLASS EXAMPLE
60              
61             This result class would work as-is with the default values of C.
62             It uses L to auto-set the C
63             and C timestamps.
64              
65             package MySchema::Result::Session;
66              
67             use strict;
68             use warnings;
69              
70             use base 'DBIx::Class::Core';
71              
72             __PACKAGE__->load_components(qw(TimeStamp));
73              
74             __PACKAGE__->table('sessions');
75              
76             __PACKAGE__->add_columns(
77             sessions_id => {
78             data_type => 'varchar', size => 255
79             },
80             session_data => {
81             data_type => 'text'
82             },
83             created => {
84             data_type => 'datetime', set_on_create => 1
85             },
86             last_modified => {
87             data_type => 'datetime', set_on_create => 1, set_on_update => 1
88             },
89             );
90              
91             __PACKAGE__->set_primary_key('sessions_id');
92              
93             1;
94              
95             =cut
96              
97 2     2   16285 use strict;
  2         2  
  2         51  
98 2     2   709 use parent 'Dancer::Session::Abstract';
  2         408  
  2         6  
99              
100 2     2   185914 use Dancer qw(:syntax !load);
  2         147651  
  2         12  
101 2     2   1736 use DBIx::Class;
  2         64699  
  2         49  
102 2     2   10 use Try::Tiny;
  2         2  
  2         81  
103 2     2   721 use Module::Load;
  2         1366  
  2         9  
104 2     2   76 use Scalar::Util qw(blessed);
  2         2  
  2         1413  
105              
106             our $VERSION = '0.005';
107              
108             my %dbic_handles;
109              
110             =head1 METHODS
111              
112             =head2 create()
113              
114             Creates a new session. Returns the session object.
115              
116             =cut
117              
118             sub create {
119 5     5 1 432870 return Dancer::Session::DBIC->new->flush;
120             }
121              
122              
123             =head2 flush()
124              
125             Write the session to the database. Returns the session object.
126              
127             =cut
128              
129             sub flush {
130 11     11 1 1233 my $self = shift;
131 11         25 my $handle = $self->_dbic;
132              
133 11         29 my %session_data = ($handle->{id_column} => $self->id,
134             $handle->{data_column} => $self->_serialize,
135             );
136              
137 11         2488 my $session = $self->_rset->update_or_create(\%session_data);
138              
139 11         62592 return $self;
140             }
141              
142             =head2 retrieve($id)
143              
144             Look for a session with the given id.
145              
146             Returns the session object if found, C if not. Logs a debug-level warning
147             if the session was found, but could not be deserialized.
148              
149             =cut
150              
151             sub retrieve {
152 19     19 1 43239 my ($self, $session_id) = @_;
153 19         26 my $session_object;
154              
155 19         42 $session_object = $self->_rset->find($session_id);
156              
157             # Bail early if we know we have no session data at all
158 19 100       35527 if (!defined $session_object) {
159 2         34 debug "Could not retrieve session ID: $session_id";
160 2         15 return;
161             }
162              
163 17         599 my $session_data = $session_object->session_data;
164              
165             # No way to check that it's valid JSON other than trying to deserialize it
166             my $session = try {
167 17     17   593 $self->_deserialize($session_data);
168             } catch {
169 0     0   0 debug "Could not deserialize session ID: $session_id - $_";
170 0         0 return;
171 17         318 };
172              
173 17 50       3826 bless $session, __PACKAGE__ if $session;
174             }
175              
176              
177             =head2 destroy()
178              
179             Remove the current session object from the database.
180              
181             =cut
182              
183             sub destroy {
184 2     2 1 175 my $self = shift;
185              
186 2 50       6 if (!defined $self->id) {
187 0         0 debug "No session ID passed to destroy method";
188 0         0 return;
189             }
190              
191 2         13 $self->_rset->find($self->id)->delete;
192             }
193              
194             # Creates and connects schema
195              
196             sub _dbic {
197 43     43   44 my $self = shift;
198              
199             # To be fork safe and thread safe, use a combination of the PID and TID (if
200             # running with use threads) to make sure no two processes/threads share
201             # handles. Implementation based on DBIx::Connector by David E. Wheeler.
202 43         61 my $pid_tid = $$;
203 43 50       97 $pid_tid .= '_' . threads->tid if $INC{'threads.pm'};
204              
205             # OK, see if we have a matching handle
206 43         66 my $handle = $dbic_handles{$pid_tid};
207              
208 43 100       107 if ($handle->{schema}) {
209 42         58 return $handle;
210             }
211              
212 1         2 my $settings = setting('session_options');
213              
214             # Prefer an active schema over a schema class.
215 1 50       11 if ( my $schema = $settings->{schema}) {
    0          
216 1 50       5 if (blessed $schema) {
    50          
217 0         0 $handle->{schema} = $schema;
218             }
219             elsif( ref $schema ) {
220 1         3 $handle->{schema} = $schema->();
221             }
222             else {
223 0 0       0 die "can't use named schema: Dancer::Plugin::DBIC not loaded\n"
224             unless $Dancer::Plugin::DBIC::VERSION;
225 0         0 $handle->{schema} = Dancer::Plugin::DBIC::schema($schema);
226             }
227             }
228             elsif (! defined $settings->{schema_class}) {
229 0         0 die "No schema class defined.";
230             }
231             else {
232 0         0 my $schema_class = $settings->{schema_class};
233              
234 0         0 $handle->{schema} = $self->_load_schema_class($schema_class,
235             $settings->{dsn},
236             $settings->{user},
237             $settings->{pass});
238             }
239              
240 1   50     10 $handle->{resultset} = $settings->{resultset} || 'Session';
241 1   50     5 $handle->{id_column} = $settings->{id_column} || 'sessions_id';
242 1   50     5 $handle->{data_column} = $settings->{data_column} || 'session_data';
243              
244 1         2 $dbic_handles{$pid_tid} = $handle;
245              
246 1         1 return $handle;
247             }
248              
249             # Returns specific resultset
250             sub _rset {
251 32     32   42 my ($self, $name) = @_;
252              
253 32         60 my $handle = $self->_dbic;
254              
255 32         114 return $handle->{schema}->resultset($handle->{resultset});
256             }
257              
258             # Loads schema class
259             sub _load_schema_class {
260 0     0   0 my ($self, $schema_class, @conn_info) = @_;
261 0         0 my ($schema_object);
262              
263 0 0       0 if ($schema_class) {
264 0         0 $schema_class =~ s/-/::/g;
265 0         0 eval { load $schema_class };
  0         0  
266 0 0       0 die "Could not load schema_class $schema_class: $@" if $@;
267 0         0 $schema_object = $schema_class->connect(@conn_info);
268             } else {
269 0         0 my $dbic_loader = 'DBIx::Class::Schema::Loader';
270 0         0 eval { load $dbic_loader };
  0         0  
271 0 0       0 die "You must provide a schema_class option or install $dbic_loader."
272             if $@;
273 0         0 $dbic_loader->naming('v7');
274 0         0 $schema_object = DBIx::Class::Schema::Loader->connect(@conn_info);
275             }
276              
277 0         0 return $schema_object;
278             }
279              
280             # Default Serialize method
281             sub _serialize {
282 11     11   59 my $self = shift;
283 11         24 my $settings = setting('session_options');
284              
285 11 50       110 if (defined $settings->{serializer}) {
286 0         0 return $settings->{serializer}->({%$self});
287             }
288              
289             # A session is by definition ephemeral - Store it compactly
290             # This is the Dancer function, not from JSON.pm
291 11         66 return to_json({%$self}, { pretty => 0, convert_blessed => 1 });
292             }
293              
294              
295             # Default Deserialize method
296             sub _deserialize {
297 17     17   44 my ($self, $json) = @_;
298 17         51 my $settings = setting('session_options');
299              
300 17 50       233 if (defined $settings->{deserializer}) {
301 0         0 return $settings->{deserializer}->($json);
302             }
303              
304             # This is the Dancer function, not from JSON.pm
305 17         57 return from_json($json, { utf8 => 0});
306             }
307              
308             =head1 SEE ALSO
309              
310             L, L
311              
312             =head1 AUTHOR
313              
314             Stefan Hornburg (Racke)
315              
316             =head1 ACKNOWLEDGEMENTS
317              
318             Based on code from L written by James Aitken
319             and code from L written by Naveed Massjouni.
320              
321             Enhancements provided by:
322              
323             Yanick Champoux (GH #6, #7).
324             Peter Mottram (GH #5).
325              
326             =head1 COPYRIGHT AND LICENSE
327              
328             This software is copyright (c) Stefan Hornburg.
329              
330             This is free software; you can redistribute it and/or modify it under
331             the same terms as the Perl 5 programming language system itself.
332              
333             =cut
334              
335              
336             1;