File Coverage

blib/lib/Dancer/Session/DBIC.pm
Criterion Covered Total %
statement 68 95 71.5
branch 12 30 40.0
condition 3 6 50.0
subroutine 16 18 88.8
pod 4 4 100.0
total 103 153 67.3


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.006
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<DBIx::Class>. The default serialization method is L<JSON>,
15             though one can specify any serialization format you want. L<YAML> and L<Storable> 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<Dancer::Plugin::DBIC>, 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<session_options>.
62             It uses L<DBIx::Class::TimeStamp> to auto-set the C<created>
63             and C<last_modified> 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   46804 use strict;
  2         2  
  2         46  
98 2     2   727 use parent 'Dancer::Session::Abstract';
  2         414  
  2         6  
99              
100 2     2   177450 use Dancer qw(:syntax !load);
  2         117097  
  2         10  
101 2     2   1449 use DBIx::Class;
  2         61031  
  2         47  
102 2     2   12 use Try::Tiny;
  2         2  
  2         85  
103 2     2   769 use Module::Load;
  2         1453  
  2         10  
104 2     2   82 use Scalar::Util qw(blessed);
  2         3  
  2         1383  
105              
106             our $VERSION = '0.006';
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 401223 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 1113 my $self = shift;
131 11         23 my $handle = $self->_dbic;
132              
133             my %session_data = ($handle->{id_column} => $self->id,
134 11         22 $handle->{data_column} => $self->_serialize,
135             );
136              
137 11         2227 my $session = $self->_rset->update_or_create(\%session_data);
138              
139 11         65804 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<undef> 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 39267 my ($self, $session_id) = @_;
153 19         21 my $session_object;
154 19         28 my $handle = $self->_dbic;
155 19         20 my $data_column = $handle->{data_column};
156              
157 19         28 $session_object = $self->_rset->find($session_id);
158              
159             # Bail early if we know we have no session data at all
160 19 100       31632 if (!defined $session_object) {
161 2         32 debug "Could not retrieve session ID: $session_id";
162 2         15 return;
163             }
164              
165 17         568 my $session_data = $session_object->$data_column;
166              
167             # No way to check that it's valid JSON other than trying to deserialize it
168             my $session = try {
169 17     17   596 $self->_deserialize($session_data);
170             } catch {
171 0     0   0 debug "Could not deserialize session ID: $session_id - $_";
172 0         0 return;
173 17         234 };
174              
175 17 50       2862 bless $session, __PACKAGE__ if $session;
176             }
177              
178              
179             =head2 destroy()
180              
181             Remove the current session object from the database.
182              
183             =cut
184              
185             sub destroy {
186 2     2 1 159 my $self = shift;
187              
188 2 50       4 if (!defined $self->id) {
189 0         0 debug "No session ID passed to destroy method";
190 0         0 return;
191             }
192              
193 2         14 $self->_rset->find($self->id)->delete;
194             }
195              
196             # Creates and connects schema
197              
198             sub _dbic {
199 62     62   55 my $self = shift;
200              
201             # To be fork safe and thread safe, use a combination of the PID and TID (if
202             # running with use threads) to make sure no two processes/threads share
203             # handles. Implementation based on DBIx::Connector by David E. Wheeler.
204 62         72 my $pid_tid = $$;
205 62 50       115 $pid_tid .= '_' . threads->tid if $INC{'threads.pm'};
206              
207             # OK, see if we have a matching handle
208 62         69 my $handle = $dbic_handles{$pid_tid};
209              
210 62 100       115 if ($handle->{schema}) {
211 61         67 return $handle;
212             }
213              
214 1         3 my $settings = setting('session_options');
215              
216             # Prefer an active schema over a schema class.
217 1 50       11 if ( my $schema = $settings->{schema}) {
    0          
218 1 50       6 if (blessed $schema) {
    50          
219 0         0 $handle->{schema} = $schema;
220             }
221             elsif( ref $schema ) {
222 1         2 $handle->{schema} = $schema->();
223             }
224             else {
225 0 0       0 die "can't use named schema: Dancer::Plugin::DBIC not loaded\n"
226             unless $Dancer::Plugin::DBIC::VERSION;
227 0         0 $handle->{schema} = Dancer::Plugin::DBIC::schema($schema);
228             }
229             }
230             elsif (! defined $settings->{schema_class}) {
231 0         0 die "No schema class defined.";
232             }
233             else {
234 0         0 my $schema_class = $settings->{schema_class};
235              
236             $handle->{schema} = $self->_load_schema_class($schema_class,
237             $settings->{dsn},
238             $settings->{user},
239 0         0 $settings->{pass});
240             }
241              
242 1   50     10 $handle->{resultset} = $settings->{resultset} || 'Session';
243 1   50     5 $handle->{id_column} = $settings->{id_column} || 'sessions_id';
244 1   50     4 $handle->{data_column} = $settings->{data_column} || 'session_data';
245              
246 1         3 $dbic_handles{$pid_tid} = $handle;
247              
248 1         2 return $handle;
249             }
250              
251             # Returns specific resultset
252             sub _rset {
253 32     32   32 my ($self, $name) = @_;
254              
255 32         44 my $handle = $self->_dbic;
256              
257 32         113 return $handle->{schema}->resultset($handle->{resultset});
258             }
259              
260             # Loads schema class
261             sub _load_schema_class {
262 0     0   0 my ($self, $schema_class, @conn_info) = @_;
263 0         0 my ($schema_object);
264              
265 0 0       0 if ($schema_class) {
266 0         0 $schema_class =~ s/-/::/g;
267 0         0 eval { load $schema_class };
  0         0  
268 0 0       0 die "Could not load schema_class $schema_class: $@" if $@;
269 0         0 $schema_object = $schema_class->connect(@conn_info);
270             } else {
271 0         0 my $dbic_loader = 'DBIx::Class::Schema::Loader';
272 0         0 eval { load $dbic_loader };
  0         0  
273 0 0       0 die "You must provide a schema_class option or install $dbic_loader."
274             if $@;
275 0         0 $dbic_loader->naming('v7');
276 0         0 $schema_object = DBIx::Class::Schema::Loader->connect(@conn_info);
277             }
278              
279 0         0 return $schema_object;
280             }
281              
282             # Default Serialize method
283             sub _serialize {
284 11     11   47 my $self = shift;
285 11         24 my $settings = setting('session_options');
286              
287 11 50       108 if (defined $settings->{serializer}) {
288 0         0 return $settings->{serializer}->({%$self});
289             }
290              
291             # A session is by definition ephemeral - Store it compactly
292             # This is the Dancer function, not from JSON.pm
293 11         51 return to_json({%$self}, { pretty => 0, convert_blessed => 1 });
294             }
295              
296              
297             # Default Deserialize method
298             sub _deserialize {
299 17     17   19 my ($self, $json) = @_;
300 17         39 my $settings = setting('session_options');
301              
302 17 50       188 if (defined $settings->{deserializer}) {
303 0         0 return $settings->{deserializer}->($json);
304             }
305              
306             # This is the Dancer function, not from JSON.pm
307 17         38 return from_json($json, { utf8 => 0});
308             }
309              
310             =head1 SEE ALSO
311              
312             L<Dancer>, L<Dancer::Session>
313              
314             =head1 AUTHOR
315              
316             Stefan Hornburg (Racke) <racke@linuxia.de>
317              
318             =head1 ACKNOWLEDGEMENTS
319              
320             Based on code from L<Dancer::Session::DBI> written by James Aitken
321             and code from L<Dancer::Plugin::DBIC> written by Naveed Massjouni.
322              
323             Enhancements provided by:
324              
325             Yanick Champoux (GH #6, #7).
326             Peter Mottram (GH #5, #8).
327              
328             =head1 COPYRIGHT AND LICENSE
329              
330             This software is copyright (c) Stefan Hornburg.
331              
332             This is free software; you can redistribute it and/or modify it under
333             the same terms as the Perl 5 programming language system itself.
334              
335             =cut
336              
337              
338             1;