File Coverage

blib/lib/PAGI/Session.pm
Criterion Covered Total %
statement 48 48 100.0
branch 20 20 100.0
condition 7 9 77.7
subroutine 14 14 100.0
pod 11 11 100.0
total 100 102 98.0


line stmt bran cond sub pod time code
1             package PAGI::Session;
2              
3 1     1   158395 use strict;
  1         2  
  1         60  
4 1     1   9 use warnings;
  1         2  
  1         38  
5 1     1   4 use Scalar::Util 'blessed';
  1         1  
  1         589  
6              
7             =head1 NAME
8              
9             PAGI::Session - Standalone helper object for session data access
10              
11             =head1 SYNOPSIS
12              
13             use PAGI::Session;
14              
15             # Construct from raw session data, scope, or request object
16             my $session = PAGI::Session->new($scope->{'pagi.session'});
17             my $session = PAGI::Session->new($scope);
18             my $session = PAGI::Session->new($req); # any object with ->scope
19              
20             # Strict get - dies if key doesn't exist (catches typos)
21             my $user_id = $session->get('user_id');
22              
23             # Safe get with default for optional keys
24             my $theme = $session->get('theme', 'light');
25              
26             # Set, delete, check existence
27             $session->set('cart_count', 3);
28             $session->delete('cart_count');
29             if ($session->exists('user_id')) { ... }
30              
31             # List user keys (excludes internal _prefixed keys)
32             my @keys = $session->keys;
33              
34             # Session lifecycle
35             $session->regenerate; # Request new session ID
36             $session->destroy; # Mark session for deletion
37              
38             =head1 DESCRIPTION
39              
40             PAGI::Session wraps the raw session data hashref and provides a clean
41             accessor interface with strict key checking. It is a standalone helper
42             that is not attached to any request or protocol object.
43              
44             The strict C method dies when a key does not exist, catching
45             typos at runtime. Use the two-argument form C
46             for keys that may or may not be present.
47              
48             =head1 CONSTRUCTOR
49              
50             =head2 new
51              
52             my $session = PAGI::Session->new($data_hashref);
53             my $session = PAGI::Session->new($scope);
54             my $session = PAGI::Session->new($request);
55              
56             Accepts raw session data (hashref), a PAGI scope (hashref with
57             C key), or any object with a C method
58             (e.g., L). The helper stores a reference to the
59             underlying hash, so mutations via C and C are
60             visible to the session middleware.
61              
62             =cut
63              
64             sub new {
65 28     28 1 186589 my ($class, $arg) = @_;
66              
67 28         33 my $data;
68 28 100 66     136 if (blessed($arg) && $arg->can('scope')) {
    100 66        
    100          
69             # Duck-typed object with scope method (e.g., PAGI::Request, PAGI::SSE)
70 1         4 $data = $arg->scope->{'pagi.session'};
71             }
72             elsif (ref $arg eq 'HASH' && exists $arg->{'pagi.session'}) {
73             # Scope hashref
74 1         3 $data = $arg->{'pagi.session'};
75             }
76             elsif (ref $arg eq 'HASH') {
77             # Raw session data hashref
78 22         27 $data = $arg;
79             }
80              
81 28 100       53 die "PAGI::Session requires session data (hashref, scope, or object with ->scope)\n"
82             unless ref $data eq 'HASH';
83              
84 24         61 return bless { _data => $data }, $class;
85             }
86              
87             =head1 METHODS
88              
89             =head2 id
90              
91             my $id = $session->id;
92              
93             Returns the session ID from C<< $data->{_id} >>.
94              
95             =cut
96              
97             sub id {
98 4     4 1 12 my ($self) = @_;
99 4         17 return $self->{_data}{_id};
100             }
101              
102             =head2 get
103              
104             my $value = $session->get('key'); # dies if missing
105             my $value = $session->get('key', $default); # returns $default if missing
106              
107             Retrieves a value from the session. With one argument, dies with an
108             error including the key name if the key does not exist. With a default
109             argument, returns the default when the key is missing (even if the
110             default is C).
111              
112             =cut
113              
114             sub get {
115 12     12 1 58 my ($self, $key, @rest) = @_;
116 12 100       23 if (!exists $self->{_data}{$key}) {
117 5 100       13 return $rest[0] if @rest;
118 2         12 die "No session key '$key'\n";
119             }
120 7         25 return $self->{_data}{$key};
121             }
122              
123             =head2 set
124              
125             $session->set('key', $value);
126             $session->set(user_id => 42, role => 'admin', email => 'john@example.com');
127              
128             Sets one or more keys in the session data. With two arguments, sets a
129             single key. With more arguments, treats them as key-value pairs.
130             Dies if given an odd number of arguments greater than one.
131              
132             =cut
133              
134             sub set {
135 8     8 1 39 my ($self, @args) = @_;
136 8 100 100     28 die "set() requires key => value pairs\n" if @args > 2 && @args % 2;
137 7 100       10 if (@args == 2) {
138 6         19 $self->{_data}{$args[0]} = $args[1];
139             }
140             else {
141 1         3 my %pairs = @args;
142 1         6 $self->{_data}{$_} = $pairs{$_} for CORE::keys %pairs;
143             }
144             }
145              
146             =head2 exists
147              
148             if ($session->exists('key')) { ... }
149              
150             Returns true if the key exists in the session data.
151              
152             =cut
153              
154             sub exists {
155 4     4 1 11 my ($self, $key) = @_;
156 4 100       16 return exists $self->{_data}{$key} ? 1 : 0;
157             }
158              
159             =head2 delete
160              
161             $session->delete('key');
162             $session->delete('k1', 'k2', 'k3');
163              
164             Removes one or more keys from the session data.
165              
166             =cut
167              
168             sub delete {
169 2     2 1 6 my ($self, @keys) = @_;
170 2         8 delete $self->{_data}{$_} for @keys;
171             }
172              
173             =head2 keys
174              
175             my @keys = $session->keys;
176              
177             Returns a list of user keys, filtering out internal keys that start
178             with an underscore (e.g. C<_id>, C<_created>, C<_last_access>).
179              
180             =cut
181              
182             sub keys {
183 4     4 1 10 my ($self) = @_;
184 4         10 return grep { !/^_/ } keys %{$self->{_data}};
  18         40  
  4         9  
185             }
186              
187             =head2 slice
188              
189             my %data = $session->slice('user_id', 'role', 'email');
190              
191             Returns a hash of key-value pairs for the requested keys. Keys that
192             do not exist in the session are silently skipped (unlike C,
193             which dies on missing keys).
194              
195             =cut
196              
197             sub slice {
198 3     3 1 15 my ($self, @keys) = @_;
199 3 100       5 return map { CORE::exists($self->{_data}{$_}) ? ($_ => $self->{_data}{$_}) : () } @keys;
  7         20  
200             }
201              
202             =head2 clear
203              
204             $session->clear;
205              
206             Removes all user keys from the session, preserving internal
207             C<_>-prefixed keys (C<_id>, C<_created>, C<_last_access>, etc.).
208             Use this for a "soft logout" that keeps the session ID but wipes
209             application data.
210              
211             =cut
212              
213             sub clear {
214 2     2 1 8 my ($self) = @_;
215 2         3 for my $key ($self->keys) {
216 3         5 delete $self->{_data}{$key};
217             }
218             }
219              
220             =head2 regenerate
221              
222             $session->regenerate;
223              
224             Requests session ID regeneration. The middleware will generate a new
225             session ID, delete the old session from the store, save session data
226             under the new ID, and update the client cookie/header.
227              
228             B Always call this after authentication (login) to prevent
229             session fixation attacks.
230              
231             =cut
232              
233             sub regenerate {
234 1     1 1 4 my ($self) = @_;
235 1         3 $self->{_data}{_regenerated} = 1;
236             }
237              
238             =head2 destroy
239              
240             $session->destroy;
241              
242             Marks the session for destruction. The middleware will delete the
243             session data from the store and clear the client-side state (e.g.,
244             expire the cookie). Use this for logout.
245              
246             =cut
247              
248             sub destroy {
249 1     1 1 6 my ($self) = @_;
250 1         4 $self->{_data}{_destroyed} = 1;
251             }
252              
253             1;
254              
255             __END__