File Coverage

blib/lib/Dancer2/Session/DBIC.pm
Criterion Covered Total %
statement 68 95 71.5
branch 11 24 45.8
condition n/a
subroutine 15 22 68.1
pod n/a
total 94 141 66.6


line stmt bran cond sub pod time code
1             package Dancer2::Session::DBIC;
2              
3 2     2   31932 use Dancer2::Core::Types;
  2         15702  
  2         463  
4 2     2   911 use DBIx::Class;
  2         78052  
  2         53  
5 2     2   847 use DBICx::Sugar;
  2         19602  
  2         83  
6 2     2   11 use Scalar::Util 'blessed';
  2         2  
  2         76  
7 2     2   7 use Module::Runtime 'use_module';
  2         3  
  2         14  
8 2     2   64 use Try::Tiny;
  2         3  
  2         94  
9              
10             our %dbic_handles;
11              
12 2     2   1000 use Moo;
  2         4125  
  2         7  
13             with 'Dancer2::Core::Role::SessionFactory';
14 2     2   2439 use namespace::clean;
  2         4  
  2         17  
15              
16             our $VERSION = '0.102';
17              
18             =head1 NAME
19              
20             Dancer2::Session::DBIC - DBIx::Class session engine for Dancer2
21              
22             =head1 VERSION
23              
24             0.102
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   12726 my $self = shift;
219 9         66 my $class = $self->serializer;
220 9 50       44 if ( $class !~ /::/ ) {
221 9         35 $class = __PACKAGE__ . "::Serializer::$class";
222             }
223              
224 9         13 my %args;
225              
226 9 50       55 $args{serialize_options} = $self->serialize_options
227             if $self->serialize_options;
228              
229 9 50       61 $args{deserialize_options} = $self->deserialize_options
230             if $self->deserialize_options;
231              
232 9         44 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   166474 my ($self, $id, $session) = @_;
275 30         136 my $handle = $self->_dbic;
276              
277             my %session_data = ($handle->{id_column} => $id,
278 30         679 $handle->{data_column} => $self->serializer_object->serialize($session),
279             );
280              
281 30         3553 $self->_rset->update_or_create(\%session_data);
282              
283 30         211376 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   655346 my ($self, $session_id) = @_;
297 72         185 my $session_object;
298              
299 72         347 $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       247772 if (!defined $session_object) {
303 12         398 die "Could not retrieve session ID: $session_id";
304 0         0 return;
305             }
306              
307 60         1524 my $data_column = $self->data_column;
308 60         1921 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   3290 $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         1535 };
317              
318 60         10423 return $session;
319             }
320              
321              
322             =head2 _destroy()
323              
324             Remove the current session object from the database.
325              
326             =cut
327              
328             # as per doc: The _destroy method must be implemented. It must take
329             # $id as a single argument and destroy the underlying data.
330              
331             sub _destroy {
332 6     6   5002 my ($self, $id) = @_;
333              
334 6 50       23 if (!defined $id) {
335 0         0 die "No session ID passed to destroy method";
336 0         0 return;
337             }
338              
339 6         28 $self->_rset->find({ $self->id_column => $id})->delete;
340             }
341              
342             # Creates and connects schema
343              
344             sub _dbic {
345 138     138   237 my $self = shift;
346              
347             # To be fork safe and thread safe, use a combination of the PID and TID (if
348             # running with use threads) to make sure no two processes/threads share
349             # handles. Implementation based on DBIx::Connector by David E. Wheeler.
350 138         381 my $pid_tid = $$;
351 138 50       614 $pid_tid .= '_' . threads->tid if $INC{'threads.pm'};
352              
353             # OK, see if we have a matching handle
354 138         373 my $handle = $dbic_handles{$pid_tid};
355              
356 138 100       454 if ($handle->{schema}) {
357 132         262 return $handle;
358             }
359              
360             # Prefer an active schema over a schema class.
361 6         24 my $schema = $self->schema;
362              
363 6 50       19 if (defined $schema) {
    0          
    0          
364 6 50       19 if (blessed $schema) {
365 0         0 $handle->{schema} = $schema;
366             }
367             else {
368 6         31 $handle->{schema} = $schema->();
369             }
370             }
371             elsif ( $self->db_connection_name ) {
372 0         0 $handle->{schema} = DBICx::Sugar::schema($self->db_connection_name);
373             }
374             elsif (! defined $self->schema_class) {
375 0         0 die "No schema class defined.";
376             }
377             else {
378 0         0 my $schema_class = $self->schema_class;
379              
380 0         0 my $settings = {};
381            
382 0         0 $handle->{schema} = $self->_load_schema_class($schema_class,
383             $self->dsn,
384             $self->user,
385             $self->password);
386             }
387              
388 6         53 $handle->{resultset} = $self->resultset;
389 6         25 $handle->{id_column} = $self->id_column;
390 6         28 $handle->{data_column} = $self->data_column;
391              
392 6         17 $dbic_handles{$pid_tid} = $handle;
393              
394 6         13 return $handle;
395             }
396              
397             # Returns specific resultset
398             sub _rset {
399 108     108   246 my ($self, $name) = @_;
400              
401 108         446 my $handle = $self->_dbic;
402              
403 108         846 return $handle->{schema}->resultset($handle->{resultset});
404             }
405              
406             # Loads schema class
407             sub _load_schema_class {
408 0     0     my ($self, $schema_class, @conn_info) = @_;
409 0           my ($schema_object);
410              
411 0 0         if ($schema_class) {
412 0           $schema_class =~ s/-/::/g;
413             try {
414 0     0     use_module($schema_class);
415             }
416             catch {
417 0     0     die "Could not load schema_class $schema_class: $_";
418 0           };
419 0           $schema_object = $schema_class->connect(@conn_info);
420             } else {
421 0           my $dbic_loader = 'DBIx::Class::Schema::Loader';
422             try {
423 0     0     use_module($dbic_loader);
424             }
425             catch {
426 0     0     die
427             "You must provide a schema_class option or install $dbic_loader.";
428 0           };
429 0           $dbic_loader->naming('v7');
430 0           $schema_object = DBIx::Class::Schema::Loader->connect(@conn_info);
431             }
432              
433 0           return $schema_object;
434             }
435              
436             =head1 SEE ALSO
437              
438             L<Dancer2>, L<Dancer2::Session>
439              
440             =head1 AUTHOR
441              
442             Stefan Hornburg (Racke) <racke@linuxia.de>
443              
444             =head1 ACKNOWLEDGEMENTS
445              
446             Based on code from L<Dancer::Session::DBI> written by James Aitken
447             and code from L<Dancer::Plugin::DBIC> written by Naveed Massjouni.
448              
449             Peter Mottram, support for JSON, YAML, Sereal and custom
450             serializers, GH #8, #9, #11, #12.
451              
452             Rory Zweistra, GH #9.
453              
454             Andy Jack, GH #2.
455              
456             =head1 COPYRIGHT AND LICENSE
457              
458             This software is copyright (c) Stefan Hornburg.
459              
460             This is free software; you can redistribute it and/or modify it under
461             the same terms as the Perl 5 programming language system itself.
462              
463             =cut
464              
465              
466             1;