@@ -42,9 +42,9 @@ class PdoSessionHandler implements \SessionHandlerInterface
42
42
private $ pdo ;
43
43
44
44
/**
45
- * @var string|null|false DNS string or null for session.save_path or false when lazy connection disabled
45
+ * @var string|null|false DSN string or null for session.save_path or false when lazy connection disabled
46
46
*/
47
- private $ dns = false ;
47
+ private $ dsn = false ;
48
48
49
49
/**
50
50
* @var string Database driver
@@ -66,6 +66,11 @@ class PdoSessionHandler implements \SessionHandlerInterface
66
66
*/
67
67
private $ dataCol ;
68
68
69
+ /**
70
+ * @var string Column for lifetime
71
+ */
72
+ private $ lifetimeCol ;
73
+
69
74
/**
70
75
* @var string Column for timestamp
71
76
*/
@@ -100,40 +105,43 @@ class PdoSessionHandler implements \SessionHandlerInterface
100
105
* Constructor.
101
106
*
102
107
* You can either pass an existing database connection as PDO instance or
103
- * pass a DNS string that will be used to lazy-connect to the database
108
+ * pass a DSN string that will be used to lazy-connect to the database
104
109
* when the session is actually used. Furthermore it's possible to pass null
105
- * which will then use the session.save_path ini setting as PDO DNS parameter.
110
+ * which will then use the session.save_path ini setting as PDO DSN parameter.
106
111
*
107
112
* List of available options:
108
113
* * db_table: The name of the table [default: sessions]
109
114
* * db_id_col: The column where to store the session id [default: sess_id]
110
115
* * db_data_col: The column where to store the session data [default: sess_data]
116
+ * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime]
111
117
* * db_time_col: The column where to store the timestamp [default: sess_time]
112
118
* * db_username: The username when lazy-connect [default: '']
113
119
* * db_password: The password when lazy-connect [default: '']
114
120
* * db_connection_options: An array of driver-specific connection options [default: array()]
115
121
*
116
- * @param \PDO|string|null $pdoOrDns A \PDO instance or DNS string or null
122
+ * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
117
123
* @param array $options An associative array of DB options
118
124
*
119
125
* @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
120
126
*/
121
- public function __construct ($ pdoOrDns , array $ options = array ())
127
+ public function __construct ($ pdoOrDsn , array $ options = array ())
122
128
{
123
- if ($ pdoOrDns instanceof \PDO ) {
124
- if (\PDO ::ERRMODE_EXCEPTION !== $ pdoOrDns ->getAttribute (\PDO ::ATTR_ERRMODE )) {
129
+ if ($ pdoOrDsn instanceof \PDO ) {
130
+ if (\PDO ::ERRMODE_EXCEPTION !== $ pdoOrDsn ->getAttribute (\PDO ::ATTR_ERRMODE )) {
125
131
throw new \InvalidArgumentException (sprintf ('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)) ' , __CLASS__ ));
126
132
}
127
133
128
- $ this ->pdo = $ pdoOrDns ;
134
+ $ this ->pdo = $ pdoOrDsn ;
135
+ $ this ->driver = $ this ->pdo ->getAttribute (\PDO ::ATTR_DRIVER_NAME );
129
136
} else {
130
- $ this ->dns = $ pdoOrDns ;
137
+ $ this ->dsn = $ pdoOrDsn ;
131
138
}
132
139
133
140
$ options = array_replace (array (
134
141
'db_table ' => 'sessions ' ,
135
142
'db_id_col ' => 'sess_id ' ,
136
143
'db_data_col ' => 'sess_data ' ,
144
+ 'db_lifetime_col ' => 'sess_lifetime ' ,
137
145
'db_time_col ' => 'sess_time ' ,
138
146
'db_username ' => '' ,
139
147
'db_password ' => '' ,
@@ -143,6 +151,7 @@ public function __construct($pdoOrDns, array $options = array())
143
151
$ this ->table = $ options ['db_table ' ];
144
152
$ this ->idCol = $ options ['db_id_col ' ];
145
153
$ this ->dataCol = $ options ['db_data_col ' ];
154
+ $ this ->lifetimeCol = $ options ['db_lifetime_col ' ];
146
155
$ this ->timeCol = $ options ['db_time_col ' ];
147
156
$ this ->username = $ options ['db_username ' ];
148
157
$ this ->password = $ options ['db_password ' ];
@@ -156,10 +165,10 @@ public function open($savePath, $sessionName)
156
165
{
157
166
$ this ->gcCalled = false ;
158
167
if (null === $ this ->pdo ) {
159
- $ this ->pdo = new \PDO ($ this ->dns ?: $ savePath , $ this ->username , $ this ->password , $ this ->connectionOptions );
168
+ $ this ->pdo = new \PDO ($ this ->dsn ?: $ savePath , $ this ->username , $ this ->password , $ this ->connectionOptions );
160
169
$ this ->pdo ->setAttribute (\PDO ::ATTR_ERRMODE , \PDO ::ERRMODE_EXCEPTION );
170
+ $ this ->driver = $ this ->pdo ->getAttribute (\PDO ::ATTR_DRIVER_NAME );
161
171
}
162
- $ this ->driver = $ this ->pdo ->getAttribute (\PDO ::ATTR_DRIVER_NAME );
163
172
164
173
return true ;
165
174
}
@@ -176,13 +185,12 @@ public function read($sessionId)
176
185
177
186
// We need to make sure we do not return session data that is already considered garbage according
178
187
// to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes.
179
- $ maxlifetime = (int ) ini_get ('session.gc_maxlifetime ' );
180
188
181
- $ sql = "SELECT $ this ->dataCol FROM $ this ->table WHERE $ this ->idCol = :id AND $ this ->timeCol > :time " ;
189
+ $ sql = "SELECT $ this ->dataCol FROM $ this ->table WHERE $ this ->idCol = :id AND $ this ->lifetimeCol + $ this -> timeCol >= :time " ;
182
190
183
191
$ stmt = $ this ->pdo ->prepare ($ sql );
184
192
$ stmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
185
- $ stmt ->bindValue (':time ' , time () - $ maxlifetime , \PDO ::PARAM_INT );
193
+ $ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
186
194
$ stmt ->execute ();
187
195
188
196
// We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
@@ -239,24 +247,28 @@ public function write($sessionId, $data)
239
247
// do an insert or update even if we created a row in read() for locking.
240
248
// We use a single MERGE SQL query when supported by the database.
241
249
250
+ $ maxlifetime = (int ) ini_get ('session.gc_maxlifetime ' );
251
+
242
252
try {
243
253
$ mergeSql = $ this ->getMergeSql ();
244
254
245
255
if (null !== $ mergeSql ) {
246
256
$ mergeStmt = $ this ->pdo ->prepare ($ mergeSql );
247
257
$ mergeStmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
248
258
$ mergeStmt ->bindParam (':data ' , $ data , \PDO ::PARAM_LOB );
259
+ $ mergeStmt ->bindParam (':lifetime ' , $ maxlifetime , \PDO ::PARAM_INT );
249
260
$ mergeStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
250
261
$ mergeStmt ->execute ();
251
262
252
263
return true ;
253
264
}
254
265
255
266
$ updateStmt = $ this ->pdo ->prepare (
256
- "UPDATE $ this ->table SET $ this ->dataCol = :data, $ this ->timeCol = :time WHERE $ this ->idCol = :id "
267
+ "UPDATE $ this ->table SET $ this ->dataCol = :data, $ this ->lifetimeCol = :lifetime, $ this -> timeCol = :time WHERE $ this ->idCol = :id "
257
268
);
258
269
$ updateStmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
259
270
$ updateStmt ->bindParam (':data ' , $ data , \PDO ::PARAM_LOB );
271
+ $ updateStmt ->bindParam (':lifetime ' , $ maxlifetime , \PDO ::PARAM_INT );
260
272
$ updateStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
261
273
$ updateStmt ->execute ();
262
274
@@ -270,10 +282,11 @@ public function write($sessionId, $data)
270
282
if (!$ updateStmt ->rowCount ()) {
271
283
try {
272
284
$ insertStmt = $ this ->pdo ->prepare (
273
- "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) "
285
+ "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) "
274
286
);
275
287
$ insertStmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
276
- $ insertStmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_LOB );
288
+ $ insertStmt ->bindParam (':data ' , $ data , \PDO ::PARAM_LOB );
289
+ $ insertStmt ->bindParam (':lifetime ' , $ maxlifetime , \PDO ::PARAM_INT );
277
290
$ insertStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
278
291
$ insertStmt ->execute ();
279
292
} catch (\PDOException $ e ) {
@@ -302,17 +315,15 @@ public function close()
302
315
$ this ->commit ();
303
316
304
317
if ($ this ->gcCalled ) {
305
- $ maxlifetime = (int ) ini_get ('session.gc_maxlifetime ' );
306
-
307
318
// delete the session records that have expired
308
- $ sql = "DELETE FROM $ this ->table WHERE $ this ->timeCol <= :time " ;
319
+ $ sql = "DELETE FROM $ this ->table WHERE $ this ->lifetimeCol + $ this -> timeCol < :time " ;
309
320
310
321
$ stmt = $ this ->pdo ->prepare ($ sql );
311
- $ stmt ->bindValue (':time ' , time () - $ maxlifetime , \PDO ::PARAM_INT );
322
+ $ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
312
323
$ stmt ->execute ();
313
324
}
314
325
315
- if (false !== $ this ->dns ) {
326
+ if (false !== $ this ->dsn ) {
316
327
$ this ->pdo = null ;
317
328
}
318
329
@@ -329,20 +340,14 @@ public function close()
329
340
*/
330
341
private function beginTransaction ()
331
342
{
332
- if ($ this ->inTransaction ) {
333
- $ this ->rollback ();
334
-
335
- throw new \BadMethodCallException (
336
- 'Session handler methods have been invoked in wrong sequence. ' .
337
- 'Expected sequence: open() -> read() -> destroy() / write() -> close() ' );
338
- }
339
-
340
- if ('sqlite ' === $ this ->driver ) {
341
- $ this ->pdo ->exec ('BEGIN IMMEDIATE TRANSACTION ' );
342
- } else {
343
- $ this ->pdo ->beginTransaction ();
343
+ if (!$ this ->inTransaction ) {
344
+ if ('sqlite ' === $ this ->driver ) {
345
+ $ this ->pdo ->exec ('BEGIN IMMEDIATE TRANSACTION ' );
346
+ } else {
347
+ $ this ->pdo ->beginTransaction ();
348
+ }
349
+ $ this ->inTransaction = true ;
344
350
}
345
- $ this ->inTransaction = true ;
346
351
}
347
352
348
353
/**
@@ -400,20 +405,20 @@ private function lockSession($sessionId)
400
405
switch ($ this ->driver ) {
401
406
case 'mysql ' :
402
407
// will also lock the row when actually nothing got updated (id = id)
403
- $ sql = "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
408
+ $ sql = "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
404
409
"ON DUPLICATE KEY UPDATE $ this ->idCol = $ this ->idCol " ;
405
410
break ;
406
411
case 'oci ' :
407
412
// DUAL is Oracle specific dummy table
408
413
$ sql = "MERGE INTO $ this ->table USING DUAL ON ( $ this ->idCol = :id) " .
409
- "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
414
+ "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
410
415
"WHEN MATCHED THEN UPDATE SET $ this ->idCol = $ this ->idCol " ;
411
416
break ;
412
417
// todo: implement locking for SQL Server < 2008
413
418
case 'sqlsrv ' === $ this ->driver && version_compare ($ this ->pdo ->getAttribute (\PDO ::ATTR_SERVER_VERSION ), '10 ' , '>= ' ):
414
419
// MS SQL Server requires MERGE be terminated by semicolon
415
420
$ sql = "MERGE INTO $ this ->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ( $ this ->idCol = :id) " .
416
- "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
421
+ "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
417
422
"WHEN MATCHED THEN UPDATE SET $ this ->idCol = $ this ->idCol ; " ;
418
423
break ;
419
424
case 'pgsql ' :
@@ -434,6 +439,7 @@ private function lockSession($sessionId)
434
439
$ stmt = $ this ->pdo ->prepare ($ sql );
435
440
$ stmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
436
441
$ stmt ->bindValue (':data ' , '' , \PDO ::PARAM_STR );
442
+ $ stmt ->bindValue (':lifetime ' , 0 , \PDO ::PARAM_INT );
437
443
$ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
438
444
$ stmt ->execute ();
439
445
}
@@ -447,21 +453,21 @@ private function getMergeSql()
447
453
{
448
454
switch ($ this ->driver ) {
449
455
case 'mysql ' :
450
- return "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
451
- "ON DUPLICATE KEY UPDATE $ this ->dataCol = VALUES( $ this ->dataCol ), $ this ->timeCol = VALUES( $ this ->timeCol ) " ;
456
+ return "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
457
+ "ON DUPLICATE KEY UPDATE $ this ->dataCol = VALUES( $ this ->dataCol ), $ this ->lifetimeCol = VALUES( $ this -> lifetimeCol ), $ this -> timeCol = VALUES( $ this ->timeCol ) " ;
452
458
case 'oci ' :
453
459
// DUAL is Oracle specific dummy table
454
460
return "MERGE INTO $ this ->table USING DUAL ON ( $ this ->idCol = :id) " .
455
- "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
456
- "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this ->timeCol = :time " ;
461
+ "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
462
+ "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this ->lifetimeCol = :lifetime, $ this -> timeCol = :time " ;
457
463
case 'sqlsrv ' === $ this ->driver && version_compare ($ this ->pdo ->getAttribute (\PDO ::ATTR_SERVER_VERSION ), '10 ' , '>= ' ):
458
464
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
459
465
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
460
466
return "MERGE INTO $ this ->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ( $ this ->idCol = :id) " .
461
- "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
462
- "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this ->timeCol = :time; " ;
467
+ "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " .
468
+ "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this ->lifetimeCol = :lifetime, $ this -> timeCol = :time; " ;
463
469
case 'sqlite ' :
464
- return "INSERT OR REPLACE INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " ;
470
+ return "INSERT OR REPLACE INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->lifetimeCol , $ this -> timeCol ) VALUES (:id, :data, :lifetime , :time) " ;
465
471
}
466
472
}
467
473
0 commit comments