Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3bd1616

Browse files
jeremylivingstonfabpot
authored andcommitted
Add LegacyPdoSessionHandler class
The changes made to the PdoSessionHandler in 2.6 introduced a backwards-compatability break for users upgrading from 2.5. This update introduces a LegacyPdoSessionHandler class that uses the old service's functionality. Users who cannot make schema updates or do not want to lose sessions can use LegacyPdoSessionHandler until 3.0.
1 parent 3402fff commit 3bd1616

File tree

3 files changed

+383
-2
lines changed

3 files changed

+383
-2
lines changed

UPGRADE-2.6.md

Lines 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
Lines 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 <[email protected]>
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 <[email protected]>
24+
* @author Michael Williams <[email protected]>
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)