File Coverage

blib/lib/Dancer/Session/DBIC.pm
Criterion Covered Total %
statement 66 91 72.5
branch 11 26 42.3
condition 3 6 50.0
subroutine 16 18 88.8
pod 4 4 100.0
total 100 145 68.9


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.004
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:
36              
37             set session_options => {schema => schema};
38              
39             Custom serializer / deserializer can be specified as follows:
40              
41             set 'session_options' => {
42             schema => schema,
43             serializer => sub { YAML::Dump(@_); },
44             deserializer => sub { YAML::Load(@_); },
45             };
46              
47             =head1 SESSION EXPIRATION
48              
49             A timestamp field that updates when a session is updated is recommended, so you can expire sessions server-side as well as client-side.
50              
51             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.
52              
53             =cut
54              
55 2     2   15556 use strict;
  2         3  
  2         67  
56 2     2   927 use parent 'Dancer::Session::Abstract';
  2         507  
  2         9  
57              
58 2     2   224216 use Dancer qw(:syntax !load);
  2         154500  
  2         13  
59 2     2   1771 use DBIx::Class;
  2         80864  
  2         67  
60 2     2   14 use Try::Tiny;
  2         3  
  2         107  
61 2     2   1666 use Module::Load;
  2         1609  
  2         13  
62 2     2   100 use Scalar::Util qw(blessed);
  2         3  
  2         1582  
63              
64             our $VERSION = '0.004';
65              
66             my %dbic_handles;
67              
68             =head1 METHODS
69              
70             =head2 create()
71              
72             Creates a new session. Returns the session object.
73              
74             =cut
75              
76             sub create {
77 5     5 1 509697 return Dancer::Session::DBIC->new->flush;
78             }
79              
80              
81             =head2 flush()
82              
83             Write the session to the database. Returns the session object.
84              
85             =cut
86              
87             sub flush {
88 9     9 1 1255 my $self = shift;
89 9         31 my $handle = $self->_dbic;
90              
91 9         39 my %session_data = ($handle->{id_column} => $self->id,
92             $handle->{data_column} => $self->_serialize,
93             );
94              
95 9         2440 my $session = $self->_rset->update_or_create(\%session_data);
96              
97 9         70114 return $self;
98             }
99              
100             =head2 retrieve($id)
101              
102             Look for a session with the given id.
103              
104             Returns the session object if found, C if not. Logs a debug-level warning
105             if the session was found, but could not be deserialized.
106              
107             =cut
108              
109             sub retrieve {
110 15     15 1 45676 my ($self, $session_id) = @_;
111 15         19 my $session_object;
112              
113 15         49 $session_object = $self->_rset->find($session_id);
114              
115             # Bail early if we know we have no session data at all
116 15 100       35326 if (!defined $session_object) {
117 2         16 debug "Could not retrieve session ID: $session_id";
118 2         22 return;
119             }
120              
121 13         360 my $session_data = $session_object->session_data;
122              
123             # No way to check that it's valid JSON other than trying to deserialize it
124             my $session = try {
125 13     13   403 $self->_deserialize($session_data);
126             } catch {
127 0     0   0 debug "Could not deserialize session ID: $session_id - $_";
128 0         0 return;
129 13         253 };
130              
131 13 50       3772 bless $session, __PACKAGE__ if $session;
132             }
133              
134              
135             =head2 destroy()
136              
137             Remove the current session object from the database.
138              
139             =cut
140              
141             sub destroy {
142 2     2 1 175 my $self = shift;
143              
144 2 50       4 if (!defined $self->id) {
145 0         0 debug "No session ID passed to destroy method";
146 0         0 return;
147             }
148              
149 2         16 $self->_rset->find($self->id)->delete;
150             }
151              
152             # Creates and connects schema
153              
154             sub _dbic {
155 35     35   50 my $self = shift;
156              
157             # To be fork safe and thread safe, use a combination of the PID and TID (if
158             # running with use threads) to make sure no two processes/threads share
159             # handles. Implementation based on DBIx::Connector by David E. Wheeler.
160 35         74 my $pid_tid = $$;
161 35 50       120 $pid_tid .= '_' . threads->tid if $INC{'threads.pm'};
162              
163             # OK, see if we have a matching handle
164 35         70 my $handle = $dbic_handles{$pid_tid};
165              
166 35 100       102 if ($handle->{schema}) {
167 34         67 return $handle;
168             }
169              
170 1         3 my $settings = setting('session_options');
171              
172             # Prefer an active schema over a schema class.
173 1 50       13 if (defined $settings->{schema}) {
    0          
174 1 50       4 if (blessed $settings->{schema}) {
175 0         0 $handle->{schema} = $settings->{schema};
176             }
177             else {
178 1         4 $handle->{schema} = $settings->{schema}->();
179             }
180             }
181             elsif (! defined $settings->{schema_class}) {
182 0         0 die "No schema class defined.";
183             }
184             else {
185 0         0 my $schema_class = $settings->{schema_class};
186              
187 0         0 $handle->{schema} = $self->_load_schema_class($schema_class,
188             $settings->{dsn},
189             $settings->{user},
190             $settings->{pass});
191             }
192              
193 1   50     12 $handle->{resultset} = $settings->{resultset} || 'Session';
194 1   50     6 $handle->{id_column} = $settings->{id_column} || 'sessions_id';
195 1   50     5 $handle->{data_column} = $settings->{data_column} || 'session_data';
196              
197 1         2 $dbic_handles{$pid_tid} = $handle;
198              
199 1         2 return $handle;
200             }
201              
202             # Returns specific resultset
203             sub _rset {
204 26     26   39 my ($self, $name) = @_;
205              
206 26         68 my $handle = $self->_dbic;
207              
208 26         137 return $handle->{schema}->resultset($handle->{resultset});
209             }
210              
211             # Loads schema class
212             sub _load_schema_class {
213 0     0   0 my ($self, $schema_class, @conn_info) = @_;
214 0         0 my ($schema_object);
215              
216 0 0       0 if ($schema_class) {
217 0         0 $schema_class =~ s/-/::/g;
218 0         0 eval { load $schema_class };
  0         0  
219 0 0       0 die "Could not load schema_class $schema_class: $@" if $@;
220 0         0 $schema_object = $schema_class->connect(@conn_info);
221             } else {
222 0         0 my $dbic_loader = 'DBIx::Class::Schema::Loader';
223 0         0 eval { load $dbic_loader };
  0         0  
224 0 0       0 die "You must provide a schema_class option or install $dbic_loader."
225             if $@;
226 0         0 $dbic_loader->naming('v7');
227 0         0 $schema_object = DBIx::Class::Schema::Loader->connect(@conn_info);
228             }
229              
230 0         0 return $schema_object;
231             }
232              
233             # Default Serialize method
234             sub _serialize {
235 9     9   62 my $self = shift;
236 9         27 my $settings = setting('session_options');
237              
238 9 50       129 if (defined $settings->{serializer}) {
239 0         0 return $settings->{serializer}->({%$self});
240             }
241              
242             # A session is by definition ephemeral - Store it compactly
243             # This is the Dancer function, not from JSON.pm
244 9         66 return to_json({%$self}, { pretty => 0 });
245             }
246              
247              
248             # Default Deserialize method
249             sub _deserialize {
250 13     13   43 my ($self, $json) = @_;
251 13         51 my $settings = setting('session_options');
252              
253 13 50       228 if (defined $settings->{deserializer}) {
254 0         0 return $settings->{deserializer}->($json);
255             }
256              
257             # This is the Dancer function, not from JSON.pm
258 13         53 return from_json($json, { utf8 => 0});
259             }
260              
261             =head1 SEE ALSO
262              
263             L, L
264              
265             =head1 AUTHOR
266              
267             Stefan Hornburg (Racke)
268              
269             =head1 ACKNOWLEDGEMENTS
270              
271             Based on code from L written by James Aitken
272             and code from L written by Naveed Massjouni.
273              
274             =head1 COPYRIGHT AND LICENSE
275              
276             This software is copyright (c) Stefan Hornburg.
277              
278             This is free software; you can redistribute it and/or modify it under
279             the same terms as the Perl 5 programming language system itself.
280              
281             =cut
282              
283              
284             1;