Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 22b183c

Browse filesBrowse files
jeremylivingstonfabpot
authored andcommitted
Add LegacyPdoSessionHandler class
1 parent 3805557 commit 22b183c
Copy full SHA for 22b183c

File tree

Expand file treeCollapse file tree

3 files changed

+383
-2
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+383
-2
lines changed

‎UPGRADE-2.6.md

Copy file name to clipboardExpand all lines: UPGRADE-2.6.md
+4-2Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
UPGRADE FROM 2.5 to 2.6
22
=======================
33

4-
Known Backwards-Compatability Breaks
4+
Known Backwards-Compatibility Breaks
55
------------------------------------
66

77
* If you use the `PdoSessionHandler`, the session table now has a different
@@ -112,7 +112,7 @@ HttpFoundation
112112
--------------
113113

114114
* The `PdoSessionHandler` to store sessions in a database changed significantly.
115-
This introduced a **backwards-compatability** break in the schema of the
115+
This introduced a **backwards-compatibility** break in the schema of the
116116
session table. The following changes must be made to your session table:
117117

118118
- Add a new integer column called `sess_lifetime`. Assuming you have the
@@ -125,6 +125,8 @@ HttpFoundation
125125
There is also an [issue](https://github.com/symfony/symfony/issues/12834)
126126
that affects Windows servers.
127127

128+
A legacy class, `LegacyPdoSessionHandler` has been created to ease backwards-compatibility issues when upgrading.
129+
128130
The changes to the `PdoSessionHandler` are:
129131
- By default, it now implements session locking to prevent loss of data by concurrent access to the same session.
130132
- It does so using a transaction between opening and closing a session. For this reason, it's not
+268Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
13+
14+
/**
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.
22+
*
23+
* @author Fabien Potencier <fabien@symfony.com>
24+
* @author Michael Williams <michael.williams@funsational.com>
25+
* @author Tobias Schultze <http://tobion.de>
26+
*
27+
* @deprecated Deprecated since version 2.6, to be removed in 3.0. Use
28+
* {@link PdoSessionHandler} instead.
29+
*/
30+
class LegacyPdoSessionHandler implements \SessionHandlerInterface
31+
{
32+
/**
33+
* @var \PDO PDO instance
34+
*/
35+
private $pdo;
36+
37+
/**
38+
* @var string Table name
39+
*/
40+
private $table;
41+
42+
/**
43+
* @var string Column for session id
44+
*/
45+
private $idCol;
46+
47+
/**
48+
* @var string Column for session data
49+
*/
50+
private $dataCol;
51+
52+
/**
53+
* @var string Column for timestamp
54+
*/
55+
private $timeCol;
56+
57+
/**
58+
* Constructor.
59+
*
60+
* List of available options:
61+
* * db_table: The name of the table [required]
62+
* * db_id_col: The column where to store the session id [default: sess_id]
63+
* * db_data_col: The column where to store the session data [default: sess_data]
64+
* * db_time_col: The column where to store the timestamp [default: sess_time]
65+
*
66+
* @param \PDO $pdo A \PDO instance
67+
* @param array $dbOptions An associative array of DB options
68+
*
69+
* @throws \InvalidArgumentException When "db_table" option is not provided
70+
*/
71+
public function __construct(\PDO $pdo, array $dbOptions = array())
72+
{
73+
if (!array_key_exists('db_table', $dbOptions)) {
74+
throw new \InvalidArgumentException('You must provide the "db_table" option for a PdoSessionStorage.');
75+
}
76+
if (\PDO::ERRMODE_EXCEPTION !== $pdo->getAttribute(\PDO::ATTR_ERRMODE)) {
77+
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__));
78+
}
79+
$this->pdo = $pdo;
80+
$dbOptions = array_merge(array(
81+
'db_id_col' => 'sess_id',
82+
'db_data_col' => 'sess_data',
83+
'db_time_col' => 'sess_time',
84+
), $dbOptions);
85+
86+
$this->table = $dbOptions['db_table'];
87+
$this->idCol = $dbOptions['db_id_col'];
88+
$this->dataCol = $dbOptions['db_data_col'];
89+
$this->timeCol = $dbOptions['db_time_col'];
90+
}
91+
92+
/**
93+
* {@inheritdoc}
94+
*/
95+
public function open($savePath, $sessionName)
96+
{
97+
return true;
98+
}
99+
100+
/**
101+
* {@inheritdoc}
102+
*/
103+
public function close()
104+
{
105+
return true;
106+
}
107+
108+
/**
109+
* {@inheritdoc}
110+
*/
111+
public function destroy($sessionId)
112+
{
113+
// delete the record associated with this id
114+
$sql = "DELETE FROM $this->table WHERE $this->idCol = :id";
115+
116+
try {
117+
$stmt = $this->pdo->prepare($sql);
118+
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
119+
$stmt->execute();
120+
} catch (\PDOException $e) {
121+
throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete a session: %s', $e->getMessage()), 0, $e);
122+
}
123+
124+
return true;
125+
}
126+
127+
/**
128+
* {@inheritdoc}
129+
*/
130+
public function gc($maxlifetime)
131+
{
132+
// delete the session records that have expired
133+
$sql = "DELETE FROM $this->table WHERE $this->timeCol < :time";
134+
135+
try {
136+
$stmt = $this->pdo->prepare($sql);
137+
$stmt->bindValue(':time', time() - $maxlifetime, \PDO::PARAM_INT);
138+
$stmt->execute();
139+
} catch (\PDOException $e) {
140+
throw new \RuntimeException(sprintf('PDOException was thrown when trying to delete expired sessions: %s', $e->getMessage()), 0, $e);
141+
}
142+
143+
return true;
144+
}
145+
146+
/**
147+
* {@inheritdoc}
148+
*/
149+
public function read($sessionId)
150+
{
151+
$sql = "SELECT $this->dataCol FROM $this->table WHERE $this->idCol = :id";
152+
153+
try {
154+
$stmt = $this->pdo->prepare($sql);
155+
$stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
156+
$stmt->execute();
157+
158+
// We use fetchAll instead of fetchColumn to make sure the DB cursor gets closed
159+
$sessionRows = $stmt->fetchAll(\PDO::FETCH_NUM);
160+
161+
if ($sessionRows) {
162+
return base64_decode($sessionRows[0][0]);
163+
}
164+
165+
return '';
166+
} catch (\PDOException $e) {
167+
throw new \RuntimeException(sprintf('PDOException was thrown when trying to read the session data: %s', $e->getMessage()), 0, $e);
168+
}
169+
}
170+
171+
/**
172+
* {@inheritdoc}
173+
*/
174+
public function write($sessionId, $data)
175+
{
176+
$encoded = base64_encode($data);
177+
178+
try {
179+
// We use a single MERGE SQL query when supported by the database.
180+
$mergeSql = $this->getMergeSql();
181+
182+
if (null !== $mergeSql) {
183+
$mergeStmt = $this->pdo->prepare($mergeSql);
184+
$mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
185+
$mergeStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
186+
$mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT);
187+
$mergeStmt->execute();
188+
189+
return true;
190+
}
191+
192+
$updateStmt = $this->pdo->prepare(
193+
"UPDATE $this->table SET $this->dataCol = :data, $this->timeCol = :time WHERE $this->idCol = :id"
194+
);
195+
$updateStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
196+
$updateStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
197+
$updateStmt->bindValue(':time', time(), \PDO::PARAM_INT);
198+
$updateStmt->execute();
199+
200+
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
201+
// duplicate key errors when the same session is written simultaneously. We can just catch such an
202+
// error and re-execute the update. This is similar to a serializable transaction with retry logic
203+
// on serialization failures but without the overhead and without possible false positives due to
204+
// longer gap locking.
205+
if (!$updateStmt->rowCount()) {
206+
try {
207+
$insertStmt = $this->pdo->prepare(
208+
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"
209+
);
210+
$insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
211+
$insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
212+
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
213+
$insertStmt->execute();
214+
} catch (\PDOException $e) {
215+
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
216+
if (0 === strpos($e->getCode(), '23')) {
217+
$updateStmt->execute();
218+
} else {
219+
throw $e;
220+
}
221+
}
222+
}
223+
} catch (\PDOException $e) {
224+
throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
225+
}
226+
227+
return true;
228+
}
229+
230+
/**
231+
* Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database.
232+
*
233+
* @return string|null The SQL string or null when not supported
234+
*/
235+
private function getMergeSql()
236+
{
237+
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
238+
239+
switch ($driver) {
240+
case 'mysql':
241+
return "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
242+
"ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->timeCol = VALUES($this->timeCol)";
243+
case 'oci':
244+
// DUAL is Oracle specific dummy table
245+
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) ".
246+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
247+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time";
248+
case 'sqlsrv' === $driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
249+
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
250+
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
251+
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) ".
252+
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) ".
253+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
254+
case 'sqlite':
255+
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
256+
}
257+
}
258+
259+
/**
260+
* Return a PDO instance
261+
*
262+
* @return \PDO
263+
*/
264+
protected function getConnection()
265+
{
266+
return $this->pdo;
267+
}
268+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.