12
12
namespace Symfony \Component \HttpFoundation \Session \Storage \Handler ;
13
13
14
14
/**
15
- * PdoSessionHandler.
15
+ * Session handler using a PDO connection to read and write data.
16
+ *
17
+ * Session data is a binary string that can contain non-printable characters like the null byte.
18
+ * For this reason this handler base64 encodes the data to be able to save it in a character column.
19
+ *
20
+ * This version of the PdoSessionHandler does NOT implement locking. So concurrent requests to the
21
+ * same session can result in data loss due to race conditions.
16
22
*
17
23
* @author Fabien Potencier <fabien@symfony.com>
18
24
* @author Michael Williams <michael.williams@funsational.com>
@@ -164,13 +170,10 @@ public function read($sessionId)
164
170
*/
165
171
public function write ($ sessionId , $ data )
166
172
{
167
- // Session data can contain non binary safe characters so we need to encode it.
168
173
$ encoded = base64_encode ($ data );
169
174
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
-
173
175
try {
176
+ // We use a MERGE SQL query when supported by the database.
174
177
$ mergeSql = $ this ->getMergeSql ();
175
178
176
179
if (null !== $ mergeSql ) {
@@ -183,28 +186,33 @@ public function write($sessionId, $data)
183
186
return true ;
184
187
}
185
188
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 ' , $ sessionId , \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) "
197
- );
198
- $ insertStmt ->bindParam (':id ' , $ sessionId , \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 ;
189
+ $ updateStmt = $ this ->pdo ->prepare (
190
+ "UPDATE $ this ->table SET $ this ->dataCol = :data, $ this ->timeCol = :time WHERE $ this ->idCol = :id "
191
+ );
192
+ $ updateStmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
193
+ $ updateStmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
194
+ $ updateStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
195
+ $ updateStmt ->execute ();
196
+
197
+ // Since Postgres does not support MERGE (without custom stored procedure), we have to use this approach
198
+ // that can result in duplicate key errors when the same session is written simultaneously. We can just
199
+ // ignore such an error because either the data did not change anyway or which data is written does not
200
+ // matter as proper locking to serialize access to a session is not implemented.
201
+ if (!$ updateStmt ->rowCount ()) {
202
+ try {
203
+ $ insertStmt = $ this ->pdo ->prepare (
204
+ "INSERT INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) "
205
+ );
206
+ $ insertStmt ->bindParam (':id ' , $ sessionId , \PDO ::PARAM_STR );
207
+ $ insertStmt ->bindParam (':data ' , $ encoded , \PDO ::PARAM_STR );
208
+ $ insertStmt ->bindValue (':time ' , time (), \PDO ::PARAM_INT );
209
+ $ insertStmt ->execute ();
210
+ } catch (\PDOException $ e ) {
211
+ // ignore unique violation SQLSTATE
212
+ if ('23505 ' !== $ e ->getCode ()) {
213
+ throw $ e ;
214
+ }
215
+ }
208
216
}
209
217
} catch (\PDOException $ e ) {
210
218
throw new \RuntimeException (sprintf ('PDOException was thrown when trying to write the session data: %s ' , $ e ->getMessage ()), 0 , $ e );
@@ -230,12 +238,12 @@ private function getMergeSql()
230
238
// DUAL is Oracle specific dummy table
231
239
return "MERGE INTO $ this ->table USING DUAL ON ( $ this ->idCol = :id) " .
232
240
"WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
233
- "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data " ;
234
- case 'sqlsrv ' :
235
- // MS SQL Server requires MERGE be terminated by semicolon
241
+ "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this -> timeCol = :time " ;
242
+ case 'sqlsrv ' && version_compare ( $ this -> pdo -> getAttribute (\ PDO :: ATTR_SERVER_VERSION ), ' 10 ' , ' >= ' ) :
243
+ // MERGE is only available since SQL Server 2008 and must be terminated by semicolon
236
244
return "MERGE INTO $ this ->table USING (SELECT 'x' AS dummy) AS src ON ( $ this ->idCol = :id) " .
237
245
"WHEN NOT MATCHED THEN INSERT ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " .
238
- "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data; " ;
246
+ "WHEN MATCHED THEN UPDATE SET $ this ->dataCol = :data, $ this -> timeCol = :time ; " ;
239
247
case 'sqlite ' :
240
248
return "INSERT OR REPLACE INTO $ this ->table ( $ this ->idCol , $ this ->dataCol , $ this ->timeCol ) VALUES (:id, :data, :time) " ;
241
249
}
0 commit comments