File Coverage

blib/lib/Dancer2/Session/DBI.pm
Criterion Covered Total %
statement 20 65 30.7
branch 0 18 0.0
condition n/a
subroutine 7 13 53.8
pod n/a
total 27 96 28.1


line stmt bran cond sub pod time code
1             package Dancer2::Session::DBI;
2              
3 1     1   87833 use 5.006;
  1         3  
4 1     1   4 use strict;
  1         1  
  1         30  
5 1     1   3 use warnings;
  1         2  
  1         43  
6              
7 1     1   471 use Moo;
  1         6092  
  1         3  
8 1     1   2108 use JSON;
  1         11773  
  1         5  
9 1     1   2541 use DBI;
  1         17931  
  1         81  
10 1     1   9 use Carp qw( carp croak );
  1         1  
  1         676  
11              
12             =encoding utf8
13              
14             =head1 NAME
15              
16             Dancer2::Session::DBI - DBI based session engine for Dancer
17              
18             =head1 VERSION
19              
20             Version 0.01
21              
22             =cut
23              
24             our $VERSION = '0.0.1';
25              
26              
27             =head1 SYNOPSIS
28              
29             This is a more or less faithful port of L to L.
30             It implements a session engine by serializing the session and storing it
31             in a database via L. The only supported serialization method is L.
32              
33             This was, so far, only tested with PostgreSQL but should in theory work
34             with MySQL and SQLite as well, as we inherit the handling of these databases
35             from the original module.
36              
37             =head1 USAGE
38              
39             In config.yml
40              
41             session: 'DBI'
42             engines:
43             session:
44             DBI:
45             dsn: "dbi:Pg:dbname=testing;host=127.0.0.1"
46             dbtable: "sessions"
47             dbuser: "user"
48             dbpass: "password"
49              
50             The table needs to have at least C and a C columns.
51              
52             A timestamp field that updates when a session is updated is recommended, so you can
53             expire sessions server-side as well as client-side.
54              
55             This session engine will not automagically remove expired sessions on the server,
56             but with a timestamp field as above, you should be able to do it manually.
57              
58             =cut
59              
60             with 'Dancer2::Core::Role::SessionFactory';
61              
62             has dsn => (
63             is => 'ro',
64             required => 1,
65             );
66              
67             has dbuser => (
68             is => 'rw',
69             );
70             has dbpass => (
71             is => 'rw',
72             );
73             has dbtable => (
74             is => 'rw',
75             required => 1,
76             );
77              
78             has quoted_table => (
79             is => 'ro',
80             builder => '_build_quoted_table'
81             );
82              
83             sub _build_quoted_table {
84 0     0     my $self = shift;
85              
86 0           return $self->dbh->quote_identifier( $self->{dbtable} );
87             };
88              
89              
90             has dbh => (
91             is => 'rw',
92             lazy => 1,
93             builder => '_build_dbh',
94             );
95              
96             sub _build_dbh {
97 0     0     my ($self) = @_;
98              
99 0           DBI->connect($self->dsn, $self->dbuser, $self->dbpass);
100             }
101              
102              
103              
104              
105              
106             sub _retrieve {
107 0     0     my ($self, $session_id) = @_;
108              
109 0           my $quoted_table = $self->quoted_table;
110              
111 0           my $sth = $self->dbh->prepare("select session_data from $quoted_table where id=?");
112 0           $sth->execute($session_id);
113 0           my ($json) = $sth->fetchrow_array();
114              
115             # Bail early if we know we have no session data at all
116 0 0         if (!defined $json) {
117 0           carp("Could not retrieve session ID: $session_id");
118 0           return;
119             }
120              
121             # No way to check that it's valid JSON other than trying to deserialize it
122 0           my $session = from_json($json);
123              
124 0           return bless $session, 'Dancer::Core::Session';
125             }
126              
127              
128             sub _flush {
129 0     0     my ($self, $id, $data) = @_;
130 0           my $json = to_json( { %{ $data } } );
  0            
131              
132 0           my $quoted_table = $self->quoted_table;
133              
134             # There is no simple cross-database way to do an "upsert"
135             # without race-conditions. So we will have to check what database driver
136             # we are using, and issue the appropriate syntax.
137 0           my $driver = lc $self->dbh->{Driver}{Name};
138              
139 0 0         if ($driver eq 'mysql') {
    0          
    0          
140              
141             # MySQL 4.1.1 made this syntax actually work. Best be extra careful
142 0 0         if ($self->dbh->{mysql_serverversion} < 40101) {
143 0           die "A minimum of MySQL 4.1.1 is required";
144             }
145              
146 0           my $sth = $self->dbh->prepare(qq{
147             INSERT INTO $quoted_table (id, session_data)
148             VALUES (?, ?)
149             ON DUPLICATE KEY
150             UPDATE session_data = ?
151             });
152              
153 0           $sth->execute($id, $json, $json);
154              
155             } elsif ($driver eq 'sqlite') {
156              
157             # All stable versions of DBD::SQLite use an SQLite version that support upserts
158 0           my $sth = $self->dbh->prepare(qq{
159             INSERT OR REPLACE INTO $quoted_table (id, session_data)
160             VALUES (?, ?)
161             });
162              
163 0           $sth->execute($id, $json);
164 0 0         $self->dbh->commit() unless $self->dbh->{AutoCommit};
165              
166             } elsif ($driver eq 'pg') {
167              
168             # Upserts need writable CTE's, which only appeared in Postgres 9.1
169 0 0         if ($self->dbh->{pg_server_version} < 90100) {
170 0           die "A minimum of PostgreSQL 9.1 is required";
171             }
172              
173 0           my $sth = $self->dbh->prepare(qq{
174             WITH upsert AS (
175             UPDATE $quoted_table
176             SET session_data = ?
177             WHERE id = ?
178             RETURNING id
179             )
180              
181             INSERT INTO $quoted_table (id, session_data)
182             SELECT ?, ?
183             WHERE NOT EXISTS (SELECT 1 FROM upsert);
184             });
185              
186 0           $sth->execute($json, $id, $id, $json);
187 0 0         $self->_dbh->commit() unless $self->dbh->{AutoCommit};
188              
189             } else {
190              
191 0           die "SQLite, MySQL > 4.1.1, and PostgreSQL > 9.1 are the only supported databases";
192              
193             }
194             }
195              
196              
197             sub _destroy {
198 0     0     my ($self, $session_id) = @_;
199              
200 0 0         if (!defined $session_id) {
201 0           carp("No session ID passed to destroy method");
202 0           return;
203             }
204              
205 0           my $quoted_table = $self->quoted_table;
206              
207 0           my $sth = $self->dbh->prepare(qq{
208             DELETE FROM $quoted_table
209             WHERE id = ?
210             });
211              
212 0           $sth->execute($session_id);
213             }
214              
215              
216             sub _sessions {
217 0     0     my ($self) = @_;
218              
219              
220 0           my $quoted_table = $self->quoted_table;
221              
222 0           my $sth = $self->dbh->prepare(qq{
223             SELECT id FROM $quoted_table
224             });
225              
226 0           $sth->execute();
227              
228 0           return $sth->fetchall_arrayref();
229             }
230              
231              
232             =head1 SEE ALSO
233              
234             L, L, L
235              
236             =head1 ACKNOWLEDGEMENTS
237              
238             This module is based on Dancer::Session::DBI by James Aitken
239              
240             =head1 LICENSE AND COPYRIGHT
241              
242             This software is Copyright (c) 2024 by Dennis Lichtenthäler.
243              
244             This is free software, licensed under:
245              
246             The Artistic License 2.0 (GPL Compatible)
247              
248              
249             =cut
250              
251             1; # End of Dancer2::Session::DBI