File Coverage

blib/lib/Dancer2/Session/DBIC.pm
Criterion Covered Total %
statement 68 97 70.1
branch 11 24 45.8
condition n/a
subroutine 15 23 65.2
pod n/a
total 94 144 65.2


line stmt bran cond sub pod time code
1             package Dancer2::Session::DBIC;
2              
3 2     2   24659 use Dancer2::Core::Types;
  2         191667  
  2         25  
4 2     2   10131 use DBIx::Class;
  2         56292  
  2         46  
5 2     2   816 use DBICx::Sugar;
  2         17869  
  2         77  
6 2     2   11 use Scalar::Util 'blessed';
  2         2  
  2         76  
7 2     2   6 use Module::Runtime 'use_module';
  2         2  
  2         12  
8 2     2   61 use Try::Tiny;
  2         2  
  2         115  
9              
10             our %dbic_handles;
11              
12 2     2   862 use Moo;
  2         3921  
  2         7  
13             with 'Dancer2::Core::Role::SessionFactory';
14 2     2   2173 use namespace::clean;
  2         3  
  2         14  
15              
16             our $VERSION = '0.110';
17              
18             =head1 NAME
19              
20             Dancer2::Session::DBIC - DBIx::Class session engine for Dancer2
21              
22             =head1 VERSION
23              
24             0.110
25              
26             =head1 DESCRIPTION
27              
28             This module implements a session engine for Dancer2 by serializing the session,
29             and storing it in a database via L<DBIx::Class>.
30              
31             JSON was chosen as the default serialization format, as it is fast, terse,
32             and portable.
33              
34             =head1 SYNOPSIS
35              
36             Example configuration:
37              
38             session: "DBIC"
39             engines:
40             session:
41             DBIC:
42             dsn: "DBI:mysql:database=testing;host=127.0.0.1;port=3306" # DBI Data Source Name
43             schema_class: "Interchange6::Schema" # DBIx::Class schema
44             user: "user" # Username used to connect to the database
45             password: "password" # Password to connect to the database
46             resultset: "MySession" # DBIx::Class resultset, defaults to Session
47             id_column: "my_session_id" # defaults to sessions_id
48             data_column: "my_session_data" # defaults to session_data
49             serializer: "YAML" # defaults to JSON
50              
51             Or if you are already using L<Dancer2::Plugin::DBIC> and want to use its
52             existing configuration for a database section named 'default' with all else
53             set to default in this module then you could simply use:
54              
55             session: "DBIC"
56             engines:
57             session:
58             DBIC:
59             db_connection_name: default
60              
61             =head1 SESSION EXPIRATION
62              
63             A timestamp field that updates when a session is updated is recommended, so you can expire sessions server-side as well as client-side.
64              
65             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.
66              
67             =cut
68              
69             =head1 ATTRIBUTES
70              
71             =head2 schema_class
72              
73             DBIx::Class schema class, e.g. L<Interchange6::Schema>.
74              
75             =cut
76              
77             has schema_class => (
78             is => 'ro',
79             isa => Str,
80             );
81              
82             =head2 db_connection_name
83              
84             The L<Dancer2::Plugin::DBIC> database connection name.
85              
86             If this option is provided then L</schema_class>, L</dsn>, L</user> and
87             L</password> are all ignored.
88              
89             =cut
90              
91             has db_connection_name => (
92             is => 'ro',
93             isa => Str,
94             );
95              
96             =head2 resultset
97              
98             DBIx::Class resultset, defaults to C<Session>.
99              
100             =cut
101              
102             has resultset => (
103             is => 'ro',
104             isa => Str,
105             default => 'Session',
106             );
107              
108             =head2 id_column
109              
110             Column for session id, defaults to C<sessions_id>.
111              
112             If this column is not the primary key of the table, it should have
113             a unique constraint added to it. See L<DBIx::Class::ResultSource/add_unique_constraint>.
114              
115             =cut
116              
117             has id_column => (
118             is => 'ro',
119             isa => Str,
120             default => 'sessions_id',
121             );
122              
123             =head2 data_column
124              
125             Column for session data, defaults to C<session_data>.
126              
127             =cut
128              
129             has data_column => (
130             is => 'ro',
131             isa => Str,
132             default => 'session_data',
133             );
134              
135             =head2 dsn
136              
137             L<DBI> dsn to connect to the database.
138              
139             =cut
140              
141             has dsn => (
142             is => 'ro',
143             isa => Str,
144             );
145              
146             =head2 user
147              
148             Database username.
149              
150             =cut
151              
152             has user => (
153             is => 'ro',
154             );
155              
156             =head2 password
157              
158             Database password.
159              
160             =cut
161              
162             has password => (
163             is => 'ro',
164             );
165              
166             =head2 schema
167              
168             L<DBIx::Class> schema.
169              
170             =cut
171              
172             has schema => (
173             is => 'ro',
174             );
175              
176             =head2 serializer
177              
178             Serializer to use, defaults to JSON.
179              
180             L<Dancer2::Session::DBIC> provides the following serializer classes:
181              
182             =over
183              
184             =item JSON - L<Dancer2::Session::DBIC::Serializer::JSON>
185              
186             =item Sereal - L<Dancer2::Session::DBIC::Serializer::Sereal>
187              
188             =item YAML - L<Dancer2::Session::DBIC::Serializer::YAML>
189              
190             =back
191              
192             If you do not use the default JSON serializer then you might need to install
193             additional modules - see the specific serializer class for details.
194              
195             You can also use your own serializer class by passing the fully-qualified class
196             name as argument to this option, e.g.: MyApp::Session::Serializer
197              
198             =cut
199              
200             has serializer => (
201             is => 'ro',
202             isa => Str,
203             default => 'JSON',
204             );
205              
206             =head2 serializer_object
207              
208             Vivified L</serializer> object.
209              
210             =cut
211              
212             has serializer_object => (
213             is => 'lazy',
214             isa => Object,
215             );
216              
217             sub _build_serializer_object {
218 9     9   9656 my $self = shift;
219 9         31 my $class = $self->serializer;
220 9 50       32 if ( $class !~ /::/ ) {
221 9         21 $class = __PACKAGE__ . "::Serializer::$class";
222             }
223              
224 9         9 my %args;
225              
226 9 50       49 $args{serialize_options} = $self->serialize_options
227             if $self->serialize_options;
228              
229 9 50       40 $args{deserialize_options} = $self->deserialize_options
230             if $self->deserialize_options;
231              
232 9         32 use_module($class)->new(%args);
233             }
234              
235             =head2 serialize_options
236              
237             Options to be passed to the constructor of the the C<serializer> class
238             as a hash reference.
239              
240             =cut
241              
242             has serialize_options => (
243             is => 'ro',
244             isa => HashRef,
245             default => sub { {} },
246             );
247              
248             =head2 deserialize_options
249              
250             Options to be passed to the constructor of the the C<deserializer> class
251             as a hash reference.
252              
253             =cut
254              
255             has deserialize_options => (
256             is => 'ro',
257             isa => HashRef,
258             default => sub { {} },
259             );
260              
261             =head1 METHODS
262              
263             =cut
264              
265 0     0   0 sub _sessions { return [] };
266              
267             =head2 _flush
268              
269             Write the session to the database. Returns the session object.
270              
271             =cut
272              
273             sub _flush {
274 30     30   139501 my ($self, $id, $session) = @_;
275 30         75 my $handle = $self->_dbic;
276              
277             my %session_data = ($handle->{id_column} => $id,
278 30         454 $handle->{data_column} => $self->serializer_object->serialize($session),
279             );
280              
281 30         2763 $self->_rset->update_or_create(\%session_data);
282              
283 30         139196 return $self;
284             }
285              
286             =head2 _retrieve($id)
287              
288             Look for a session with the given id.
289              
290             Returns the session object if found, C<undef> if not.
291             Dies if the session was found, but could not be deserialized.
292              
293             =cut
294              
295             sub _retrieve {
296 72     72   458254 my ($self, $session_id) = @_;
297 72         87 my $session_object;
298              
299 72         147 $session_object = $self->_rset->find({ $self->id_column => $session_id });
300              
301             # Bail early if we know we have no session data at all
302 72 100       148231 if (!defined $session_object) {
303 12         269 die "Could not retrieve session ID: $session_id";
304 0         0 return;
305             }
306              
307 60         896 my $data_column = $self->data_column;
308 60         1345 my $session_data = $session_object->$data_column;
309              
310             # No way to check that it's valid JSON other than trying to deserialize it
311             my $session = try {
312 60     60   2290 $self->serializer_object->deserialize($session_data);
313             } catch {
314 0     0   0 die "Could not deserialize session ID: $session_id - $_";
315 0         0 return;
316 60         904 };
317              
318 60         7849 return $session;
319             }
320              
321             =head2 _change_id( $old_id, $new_id )
322              
323             Change ID of session with C<$old_id> to <$new_id>.
324              
325             =cut
326              
327             sub _change_id {
328 0     0   0 my ( $self, $old_id, $new_id ) = @_;
329              
330 0         0 $self->_rset->search( { $self->id_column => $old_id } )
331             ->update( { $self->id_column => $new_id } );
332             }
333              
334             =head2 _destroy()
335              
336             Remove the current session object from the database.
337              
338             =cut
339              
340             # as per doc: The _destroy method must be implemented. It must take
341             # $id as a single argument and destroy the underlying data.
342              
343             sub _destroy {
344 6     6   3463 my ($self, $id) = @_;
345              
346 6 50       22 if (!defined $id) {
347 0         0 die "No session ID passed to destroy method";
348 0         0 return;
349             }
350              
351 6         17 $self->_rset->find({ $self->id_column => $id})->delete;
352             }
353              
354             # Creates and connects schema
355              
356             sub _dbic {
357 138     138   137 my $self = shift;
358              
359             # To be fork safe and thread safe, use a combination of the PID and TID (if
360             # running with use threads) to make sure no two processes/threads share
361             # handles. Implementation based on DBIx::Connector by David E. Wheeler.
362 138         175 my $pid_tid = $$;
363 138 50       319 $pid_tid .= '_' . threads->tid if $INC{'threads.pm'};
364              
365             # OK, see if we have a matching handle
366 138         187 my $handle = $dbic_handles{$pid_tid};
367              
368 138 100       286 if ($handle->{schema}) {
369 132         167 return $handle;
370             }
371              
372             # Prefer an active schema over a schema class.
373 6         16 my $schema = $self->schema;
374              
375 6 50       15 if (defined $schema) {
    0          
    0          
376 6 50       15 if (blessed $schema) {
377 0         0 $handle->{schema} = $schema;
378             }
379             else {
380 6         16 $handle->{schema} = $schema->();
381             }
382             }
383             elsif ( $self->db_connection_name ) {
384 0         0 $handle->{schema} = DBICx::Sugar::schema($self->db_connection_name);
385             }
386             elsif (! defined $self->schema_class) {
387 0         0 die "No schema class defined.";
388             }
389             else {
390 0         0 my $schema_class = $self->schema_class;
391              
392 0         0 my $settings = {};
393            
394 0         0 $handle->{schema} = $self->_load_schema_class($schema_class,
395             $self->dsn,
396             $self->user,
397             $self->password);
398             }
399              
400 6         31 $handle->{resultset} = $self->resultset;
401 6         17 $handle->{id_column} = $self->id_column;
402 6         16 $handle->{data_column} = $self->data_column;
403              
404 6         10 $dbic_handles{$pid_tid} = $handle;
405              
406 6         9 return $handle;
407             }
408              
409             # Returns specific resultset
410             sub _rset {
411 108     108   112 my ($self, $name) = @_;
412              
413 108         220 my $handle = $self->_dbic;
414              
415 108         348 return $handle->{schema}->resultset($handle->{resultset});
416             }
417              
418             # Loads schema class
419             sub _load_schema_class {
420 0     0     my ($self, $schema_class, @conn_info) = @_;
421 0           my ($schema_object);
422              
423 0 0         if ($schema_class) {
424 0           $schema_class =~ s/-/::/g;
425             try {
426 0     0     use_module($schema_class);
427             }
428             catch {
429 0     0     die "Could not load schema_class $schema_class: $_";
430 0           };
431 0           $schema_object = $schema_class->connect(@conn_info);
432             } else {
433 0           my $dbic_loader = 'DBIx::Class::Schema::Loader';
434             try {
435 0     0     use_module($dbic_loader);
436             }
437             catch {
438 0     0     die
439             "You must provide a schema_class option or install $dbic_loader.";
440 0           };
441 0           $dbic_loader->naming('v7');
442 0           $schema_object = DBIx::Class::Schema::Loader->connect(@conn_info);
443             }
444              
445 0           return $schema_object;
446             }
447              
448             =head1 SEE ALSO
449              
450             L<Dancer2>, L<Dancer2::Session>
451              
452             =head1 AUTHOR
453              
454             Stefan Hornburg (Racke) <racke@linuxia.de>
455              
456             =head1 ACKNOWLEDGEMENTS
457              
458             Based on code from L<Dancer::Session::DBI> written by James Aitken
459             and code from L<Dancer::Plugin::DBIC> written by Naveed Massjouni.
460              
461             Peter Mottram, support for JSON, YAML, Sereal and custom
462             serializers, GH #8, #9, #11, #12. Also for adding _change_id
463             method and accompanying tests.
464              
465             Rory Zweistra, GH #9.
466              
467             Andy Jack, GH #2.
468              
469             =head1 COPYRIGHT AND LICENSE
470              
471             This software is copyright (c) Stefan Hornburg.
472              
473             This is free software; you can redistribute it and/or modify it under
474             the same terms as the Perl 5 programming language system itself.
475              
476             =cut
477              
478              
479             1;