16
16
*
17
17
* @author Fabien Potencier <[email protected] >
18
18
* @author Michael Williams <[email protected] >
19
+ * @author Tobias Schultze <http://tobion.de>
19
20
*/
20
21
class PdoSessionHandler implements \SessionHandlerInterface
21
22
{
22
23
/**
23
- * @var \PDO PDO instance.
24
+ * @var \PDO PDO instance
24
25
*/
25
26
private $ pdo ;
26
27
27
28
/**
28
- * @var array Database options.
29
+ * @var string Table name
29
30
*/
30
- private $ dbOptions ;
31
+ private $ table ;
32
+
33
+ /**
34
+ * @var string Column for session id
35
+ */
36
+ private $ idCol ;
37
+
38
+ /**
39
+ * @var string Column for session data
40
+ */
41
+ private $ dataCol ;
42
+
43
+ /**
44
+ * @var string Column for timestamp
45
+ */
46
+ private $ timeCol ;
31
47
32
48
/**
33
49
* Constructor.
@@ -52,11 +68,16 @@ public function __construct(\PDO $pdo, array $dbOptions = array())
52
68
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__ ));
53
69
}
54
70
$ this ->pdo = $ pdo ;
55
- $ this -> dbOptions = array_merge (array (
71
+ $ dbOptions = array_merge (array (
56
72
'db_id_col ' => 'sess_id ' ,
57
73
'db_data_col ' => 'sess_data ' ,
58
74
'db_time_col ' => 'sess_time ' ,
59
75
), $ dbOptions );
76
+
77
+ $ this ->table = $ dbOptions ['db_table ' ];
78
+ $ this ->idCol = $ dbOptions ['db_id_col ' ];
79
+ $ this ->dataCol = $ dbOptions ['db_data_col ' ];
80
+ $ this ->timeCol = $ dbOptions ['db_time_col ' ];
60
81
}
61
82
62
83
/**
@@ -80,19 +101,15 @@ public function close()
80
101
*/
81
102
public function destroy ($ id )
82
103
{
83
- // get table/column
84
- $ dbTable = $ this ->dbOptions ['db_table ' ];
85
- $ dbIdCol = $ this ->dbOptions ['db_id_col ' ];
86
-
87
104
// delete the record associated with this id
88
- $ sql = "DELETE FROM $ dbTable WHERE $ dbIdCol = :id " ;
105
+ $ sql = "DELETE FROM $ this -> table WHERE $ this -> idCol = :id " ;
89
106
90
107
try {
91
108
$ stmt = $ this ->pdo ->prepare ($ sql );
92
109
$ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
93
110
$ stmt ->execute ();
94
111
} catch (\PDOException $ e ) {
95
- throw new \RuntimeException (sprintf ('PDOException was thrown when trying to manipulate session data : %s ' , $ e ->getMessage ()), 0 , $ e );
112
+ throw new \RuntimeException (sprintf ('PDOException was thrown when trying to delete a session : %s ' , $ e ->getMessage ()), 0 , $ e );
96
113
}
97
114
98
115
return true ;
@@ -103,19 +120,15 @@ public function destroy($id)
103
120
*/
104
121
public function gc ($ lifetime )
105
122
{
106
- // get table/column
107
- $ dbTable = $ this ->dbOptions ['db_table ' ];
108
- $ dbTimeCol = $ this ->dbOptions ['db_time_col ' ];
109
-
110
123
// delete the session records that have expired
111
- $ sql = "DELETE FROM $ dbTable WHERE $ dbTimeCol < :time " ;
124
+ $ sql = "DELETE FROM $ this -> table WHERE $ this -> timeCol < :time " ;
112
125
113
126
try {
114
127
$ stmt = $ this ->pdo ->prepare ($ sql );
115
128
$ stmt ->bindValue (':time ' , time () - $ lifetime , \PDO ::PARAM_INT );
116
129
$ stmt ->execute ();
117
130
} catch (\PDOException $ e ) {
118
- throw new \RuntimeException (sprintf ('PDOException was thrown when trying to manipulate session data : %s ' , $ e ->getMessage ()), 0 , $ e );
131
+ throw new \RuntimeException (sprintf ('PDOException was thrown when trying to delete expired sessions : %s ' , $ e ->getMessage ()), 0 , $ e );
119
132
}
120
133
121
134
return true ;
@@ -126,29 +139,20 @@ public function gc($lifetime)
126
139
*/
127
140
public function read ($ id )
128
141
{
129
- // get table/columns
130
- $ dbTable = $ this ->dbOptions ['db_table ' ];
131
- $ dbDataCol = $ this ->dbOptions ['db_data_col ' ];
132
- $ dbIdCol = $ this ->dbOptions ['db_id_col ' ];
142
+ $ sql = "SELECT $ this ->dataCol FROM $ this ->table WHERE $ this ->idCol = :id " ;
133
143
134
144
try {
135
- $ sql = "SELECT $ dbDataCol FROM $ dbTable WHERE $ dbIdCol = :id " ;
136
-
137
145
$ stmt = $ this ->pdo ->prepare ($ sql );
138
146
$ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
139
-
140
147
$ stmt ->execute ();
141
- // it is recommended to use fetchAll so that PDO can close the DB cursor
142
- // we anyway expect either no rows, or one row with one column. fetchColumn, seems to be buggy #4777
148
+
149
+ // We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
143
150
$ sessionRows = $ stmt ->fetchAll (\PDO ::FETCH_NUM );
144
151
145
- if (count ( $ sessionRows) == 1 ) {
152
+ if ($ sessionRows ) {
146
153
return base64_decode ($ sessionRows [0 ][0 ]);
147
154
}
148
155
149
- // session does not exist, create it
150
- $ this ->createNewSession ($ id );
151
-
152
156
return '' ;
153
157
} catch (\PDOException $ e ) {
154
158
throw new \RuntimeException (sprintf ('PDOException was thrown when trying to read the session data: %s ' , $ e ->getMessage ()), 0 , $ e );
@@ -160,84 +164,74 @@ public function read($id)
160
164
*/
161
165
public function write ($ id , $ data )
162
166
{
163
- // get table/column
164
- $ dbTable = $ this ->dbOptions ['db_table ' ];
165
- $ dbDataCol = $ this ->dbOptions ['db_data_col ' ];
166
- $ dbIdCol = $ this ->dbOptions ['db_id_col ' ];
167
- $ dbTimeCol = $ this ->dbOptions ['db_time_col ' ];
168
-
169
- //session data can contain non binary safe characters so we need to encode it
167
+ // Session data can contain non binary safe characters so we need to encode it.
170
168
$ encoded = base64_encode ($ data );
171
169
170
+ // We use a MERGE SQL query when supported by the database.
171
+ // Otherwise we have to use a transactional DELETE followed by INSERT to prevent duplicate entries under high concurrency.
172
+
172
173
try {
173
- $ driver = $ this ->pdo ->getAttribute (\PDO ::ATTR_DRIVER_NAME );
174
-
175
- if ('mysql ' === $ driver ) {
176
- // MySQL would report $stmt->rowCount() = 0 on UPDATE when the data is left unchanged
177
- // it could result in calling createNewSession() whereas the session already exists in
178
- // the DB which would fail as the id is unique
179
- $ stmt = $ this ->pdo ->prepare (
180
- "INSERT INTO $ dbTable ( $ dbIdCol, $ dbDataCol, $ dbTimeCol) VALUES (:id, :data, :time) " .
181
- "ON DUPLICATE KEY UPDATE $ dbDataCol = VALUES( $ dbDataCol), $ dbTimeCol = VALUES( $ dbTimeCol) "
174
+ $ mergeSql = $ this ->getMergeSql ();
175
+
176
+ if (null !== $ mergeSql ) {
177
+ $ mergeStmt = $ this ->pdo ->prepare ($ mergeSql );
178
+ $ mergeStmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
179
+ $ mergeStmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
180
+ $ mergeStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
181
+ $ mergeStmt ->execute ();
182
+
183
+ return true ;
184
+ }
185
+
186
+ $ this ->pdo ->beginTransaction ();
187
+
188
+ try {
189
+ $ deleteStmt = $ this ->pdo ->prepare (
190
+ "DELETE FROM $ this ->table WHERE $ this ->idCol = :id "
191
+ );
192
+ $ deleteStmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
193
+ $ deleteStmt ->execute ();
194
+
195
+ $ insertStmt = $ this ->pdo ->prepare (
196
+ "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) "
182
197
);
183
- $ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
184
- $ stmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
185
- $ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
186
- $ stmt ->execute ();
187
- } elseif ('oci ' === $ driver ) {
188
- $ stmt = $ this ->pdo ->prepare ("MERGE INTO $ dbTable USING DUAL ON( $ dbIdCol = :id) " .
189
- "WHEN NOT MATCHED THEN INSERT ( $ dbIdCol, $ dbDataCol, $ dbTimeCol) VALUES (:id, :data, sysdate) " .
190
- "WHEN MATCHED THEN UPDATE SET $ dbDataCol = :data WHERE $ dbIdCol = :id " );
191
-
192
- $ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
193
- $ stmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
194
- $ stmt ->execute ();
195
- } else {
196
- $ stmt = $ this ->pdo ->prepare ("UPDATE $ dbTable SET $ dbDataCol = :data, $ dbTimeCol = :time WHERE $ dbIdCol = :id " );
197
- $ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
198
- $ stmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
199
- $ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
200
- $ stmt ->execute ();
201
-
202
- if (!$ stmt ->rowCount ()) {
203
- // No session exists in the database to update. This happens when we have called
204
- // session_regenerate_id()
205
- $ this ->createNewSession ($ id , $ data );
206
- }
198
+ $ insertStmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
199
+ $ insertStmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
200
+ $ insertStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
201
+ $ insertStmt ->execute ();
202
+
203
+ $ this ->pdo ->commit ();
204
+ } catch (\PDOException $ e ) {
205
+ $ this ->pdo ->rollback ();
206
+
207
+ throw $ e ;
207
208
}
208
209
} catch (\PDOException $ e ) {
209
- throw new \RuntimeException (sprintf ('PDOException was thrown when trying to write the session data: %s ' , $ e ->getMessage ()), 0 , $ e );
210
+ throw new \RuntimeException (sprintf ('PDOException was thrown when trying to write the session data: %s ' , $ e ->getMessage ()), 0 , $ e );
210
211
}
211
212
212
213
return true ;
213
214
}
214
215
215
216
/**
216
- * Creates a new session with the given $id and $data
217
+ * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
217
218
*
218
- * @param string $id
219
- * @param string $data
220
- *
221
- * @return boolean True.
219
+ * @return string|null The SQL string or null when not supported
222
220
*/
223
- private function createNewSession ( $ id , $ data = '' )
221
+ private function getMergeSql ( )
224
222
{
225
- // get table/column
226
- $ dbTable = $ this ->dbOptions ['db_table ' ];
227
- $ dbDataCol = $ this ->dbOptions ['db_data_col ' ];
228
- $ dbIdCol = $ this ->dbOptions ['db_id_col ' ];
229
- $ dbTimeCol = $ this ->dbOptions ['db_time_col ' ];
230
-
231
- $ sql = "INSERT INTO $ dbTable ( $ dbIdCol, $ dbDataCol, $ dbTimeCol) VALUES (:id, :data, :time) " ;
232
-
233
- //session data can contain non binary safe characters so we need to encode it
234
- $ encoded = base64_encode ($ data );
235
- $ stmt = $ this ->pdo ->prepare ($ sql );
236
- $ stmt ->bindParam (':id ' , $ id , \PDO ::PARAM_STR );
237
- $ stmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
238
- $ stmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
239
- $ stmt ->execute ();
223
+ $ driver = $ this ->pdo ->getAttribute (\PDO ::ATTR_DRIVER_NAME );
224
+
225
+ switch ($ driver ) {
226
+ case 'mysql ' :
227
+ return "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
228
+ "ON DUPLICATE KEY UPDATE $ this ->dataCol = VALUES( $ this ->dataCol ), $ this ->timeCol = VALUES( $ this ->timeCol ) " ;
229
+ case 'oci ' :
230
+ return "MERGE INTO $ this ->table USING DUAL ON ( $ this ->idCol = :id) " .
231
+ "WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
232
+ "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data " ;
233
+ }
240
234
241
- return true ;
235
+ return null ;
242
236
}
243
237
}
0 commit comments