Skip to content

Commit d11199a

Browse files
committed
Merge branch '2.3' into 2.4
* 2.3: [Bridge][Twig] Replace deprecated features [HttpFoundation] fix switch statement [Doctrine Bridge] fix DBAL session handler according to PdoSessionHandler fixed previous merge Added phpdoc for Cache-Control directives methods Remove undefined variable $e bumped Symfony version to 2.3.17 Fix a parameter name in a test updated VERSION for 2.3.16 update CONTRIBUTORS for 2.3.16 updated CHANGELOG for 2.3.16 [HttpFoundation] use different approach for duplicate keys in postgres, fix merge for sqlsrv and oracle Conflicts: src/Symfony/Component/DependencyInjection/ContainerBuilder.php src/Symfony/Component/HttpKernel/Kernel.php
2 parents 138498f + 5ee6f13 commit d11199a

File tree

3 files changed

+73
-36
lines changed

3 files changed

+73
-36
lines changed

HeaderBag.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,23 +240,48 @@ public function getDate($key, \DateTime $default = null)
240240
return $date;
241241
}
242242

243+
/**
244+
* Adds a custom Cache-Control directive.
245+
*
246+
* @param string $key The Cache-Control directive name
247+
* @param mixed $value The Cache-Control directive value
248+
*/
243249
public function addCacheControlDirective($key, $value = true)
244250
{
245251
$this->cacheControl[$key] = $value;
246252

247253
$this->set('Cache-Control', $this->getCacheControlHeader());
248254
}
249255

256+
/**
257+
* Returns true if the Cache-Control directive is defined.
258+
*
259+
* @param string $key The Cache-Control directive
260+
*
261+
* @return bool true if the directive exists, false otherwise
262+
*/
250263
public function hasCacheControlDirective($key)
251264
{
252265
return array_key_exists($key, $this->cacheControl);
253266
}
254267

268+
/**
269+
* Returns a Cache-Control directive value by name.
270+
*
271+
* @param string $key The directive name
272+
*
273+
* @return mixed|null The directive value if defined, null otherwise
274+
*/
255275
public function getCacheControlDirective($key)
256276
{
257277
return array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null;
258278
}
259279

280+
/**
281+
* Removes a Cache-Control directive.
282+
*
283+
* @param string $key The Cache-Control directive
284+
*/
260285
public function removeCacheControlDirective($key)
261286
{
262287
unset($this->cacheControl[$key]);

Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
1313

1414
/**
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.
1622
*
1723
* @author Fabien Potencier <fabien@symfony.com>
1824
* @author Michael Williams <michael.williams@funsational.com>
@@ -164,13 +170,10 @@ public function read($sessionId)
164170
*/
165171
public function write($sessionId, $data)
166172
{
167-
// Session data can contain non binary safe characters so we need to encode it.
168173
$encoded = base64_encode($data);
169174

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-
173175
try {
176+
// We use a single MERGE SQL query when supported by the database.
174177
$mergeSql = $this->getMergeSql();
175178

176179
if (null !== $mergeSql) {
@@ -183,28 +186,36 @@ public function write($sessionId, $data)
183186
return true;
184187
}
185188

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+
// When MERGE is not supported, like in Postgres, we have to use this approach that can result in
198+
// duplicate key errors when the same session is written simultaneously. We can just catch such an
199+
// error and re-execute the update. This is similar to a serializable transaction with retry logic
200+
// on serialization failures but without the overhead and without possible false positives due to
201+
// longer gap locking.
202+
if (!$updateStmt->rowCount()) {
203+
try {
204+
$insertStmt = $this->pdo->prepare(
205+
"INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)"
206+
);
207+
$insertStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR);
208+
$insertStmt->bindParam(':data', $encoded, \PDO::PARAM_STR);
209+
$insertStmt->bindValue(':time', time(), \PDO::PARAM_INT);
210+
$insertStmt->execute();
211+
} catch (\PDOException $e) {
212+
// Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys
213+
if (0 === strpos($e->getCode(), '23')) {
214+
$updateStmt->execute();
215+
} else {
216+
throw $e;
217+
}
218+
}
208219
}
209220
} catch (\PDOException $e) {
210221
throw new \RuntimeException(sprintf('PDOException was thrown when trying to write the session data: %s', $e->getMessage()), 0, $e);
@@ -230,12 +241,13 @@ private function getMergeSql()
230241
// DUAL is Oracle specific dummy table
231242
return "MERGE INTO $this->table USING DUAL ON ($this->idCol = :id) " .
232243
"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
236-
return "MERGE INTO $this->table USING (SELECT 'x' AS dummy) AS src ON ($this->idCol = :id) " .
244+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time";
245+
case 'sqlsrv' === $driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='):
246+
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
247+
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
248+
return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = :id) " .
237249
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time) " .
238-
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data;";
250+
"WHEN MATCHED THEN UPDATE SET $this->dataCol = :data, $this->timeCol = :time;";
239251
case 'sqlite':
240252
return "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->timeCol) VALUES (:id, :data, :time)";
241253
}

Tests/Session/Storage/Handler/PdoSessionHandlerTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ protected function setUp()
2323
$this->markTestSkipped('This test requires SQLite support in your environment');
2424
}
2525

26-
$this->pdo = new \PDO("sqlite::memory:");
26+
$this->pdo = new \PDO('sqlite::memory:');
2727
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
28-
$sql = "CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)";
28+
$sql = 'CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)';
2929
$this->pdo->exec($sql);
3030
}
3131

@@ -37,9 +37,9 @@ public function testIncompleteOptions()
3737

3838
public function testWrongPdoErrMode()
3939
{
40-
$pdo = new \PDO("sqlite::memory:");
40+
$pdo = new \PDO('sqlite::memory:');
4141
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_SILENT);
42-
$pdo->exec("CREATE TABLE sessions (sess_id VARCHAR(255) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)");
42+
$pdo->exec('CREATE TABLE sessions (sess_id VARCHAR(128) PRIMARY KEY, sess_data TEXT, sess_time INTEGER)');
4343

4444
$this->setExpectedException('InvalidArgumentException');
4545
$storage = new PdoSessionHandler($pdo, array('db_table' => 'sessions'));

0 commit comments

Comments
 (0)