mirror of
https://github.com/symfony/cache.git
synced 2026-03-23 23:22:07 +01:00
Merge branch '7.4' into 8.0
* 7.4: [Serializer] Fix handling of constructor enum denormalization errors [Console] ProgressIndicator console helper display with multiple processes [HttpFoundation] Handle empty session data in updateTimestamp() to fix compat with PHP 8.6 [Console] Fix arguments set via #[Ask] wrongly considered null in profiler [Cache] Wrap `DoctrineDbalAdapter::doSave()` in savepoint to prevent transaction poisoning Update security-1.0.xsd with missing oauth2 element [Console] Silence shell_exec warning in hasSttyAvailable
This commit is contained in:
@@ -38,6 +38,8 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
|
||||
{
|
||||
private const MAX_KEY_LENGTH = 255;
|
||||
|
||||
private static int $savepointCounter = 0;
|
||||
|
||||
private MarshallerInterface $marshaller;
|
||||
private Connection $conn;
|
||||
private string $platformName;
|
||||
@@ -241,6 +243,26 @@ class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
|
||||
return $failed;
|
||||
}
|
||||
|
||||
if ($this->conn->isTransactionActive() && $this->conn->getDatabasePlatform()->supportsSavepoints()) {
|
||||
$savepoint = 'cache_save_'.++self::$savepointCounter;
|
||||
try {
|
||||
$this->conn->createSavepoint($savepoint);
|
||||
$failed = $this->doSaveInner($values, $lifetime, $failed);
|
||||
$this->conn->releaseSavepoint($savepoint);
|
||||
|
||||
return $failed;
|
||||
} catch (\Throwable $e) {
|
||||
$this->conn->rollbackSavepoint($savepoint);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->doSaveInner($values, $lifetime, $failed);
|
||||
}
|
||||
|
||||
private function doSaveInner(array $values, int $lifetime, array $failed): array|bool
|
||||
{
|
||||
$platformName = $this->getPlatformName();
|
||||
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ use Doctrine\DBAL\Driver\AbstractMySQLDriver;
|
||||
use Doctrine\DBAL\Driver\Middleware;
|
||||
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
@@ -82,7 +83,7 @@ class DoctrineDbalAdapterTest extends AdapterTestCase
|
||||
$schema = new Schema();
|
||||
|
||||
$adapter = new DoctrineDbalAdapter($connection);
|
||||
$adapter->configureSchema($schema, $connection, fn () => true);
|
||||
$adapter->configureSchema($schema, $connection, static fn () => true);
|
||||
$this->assertTrue($schema->hasTable('cache_items'));
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ class DoctrineDbalAdapterTest extends AdapterTestCase
|
||||
$schema = new Schema();
|
||||
|
||||
$adapter = $this->createCachePool();
|
||||
$adapter->configureSchema($schema, $otherConnection, fn () => false);
|
||||
$adapter->configureSchema($schema, $otherConnection, static fn () => false);
|
||||
$this->assertFalse($schema->hasTable('cache_items'));
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ class DoctrineDbalAdapterTest extends AdapterTestCase
|
||||
$schema->createTable('cache_items');
|
||||
|
||||
$adapter = new DoctrineDbalAdapter($connection);
|
||||
$adapter->configureSchema($schema, $connection, fn () => true);
|
||||
$adapter->configureSchema($schema, $connection, static fn () => true);
|
||||
$table = $schema->getTable('cache_items');
|
||||
$this->assertSame([], $table->getColumns(), 'The table was not overwritten');
|
||||
}
|
||||
@@ -160,6 +161,49 @@ class DoctrineDbalAdapterTest extends AdapterTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testSaveWithinActiveTransactionUsesSavepoint()
|
||||
{
|
||||
$dbFile = tempnam(sys_get_temp_dir(), 'sf_sqlite_savepoint');
|
||||
try {
|
||||
$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => $dbFile], $this->getDbalConfig());
|
||||
$adapter = new DoctrineDbalAdapter($connection);
|
||||
$adapter->createTable();
|
||||
|
||||
$connection->beginTransaction();
|
||||
$item = $adapter->getItem('savepoint_key');
|
||||
$item->set('savepoint_value');
|
||||
$adapter->save($item);
|
||||
|
||||
$this->assertTrue($connection->isTransactionActive(), 'Outer transaction must still be active after cache save');
|
||||
$connection->commit();
|
||||
|
||||
$this->assertSame('savepoint_value', $adapter->getItem('savepoint_key')->get());
|
||||
} finally {
|
||||
@unlink($dbFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testSavepointIsRolledBackOnFailure()
|
||||
{
|
||||
$platform = $this->createMock(AbstractPlatform::class);
|
||||
$platform->method('supportsSavepoints')->willReturn(true);
|
||||
|
||||
$conn = $this->createMock(Connection::class);
|
||||
$conn->method('isTransactionActive')->willReturn(true);
|
||||
$conn->method('getDatabasePlatform')->willReturn($platform);
|
||||
$conn->expects($this->once())->method('createSavepoint')->with($this->stringStartsWith('cache_save_'));
|
||||
$conn->expects($this->once())->method('rollbackSavepoint')->with($this->stringStartsWith('cache_save_'));
|
||||
$conn->expects($this->never())->method('releaseSavepoint');
|
||||
$conn->method('prepare')->willThrowException(new \RuntimeException('DB error'));
|
||||
|
||||
$adapter = new DoctrineDbalAdapter($conn);
|
||||
|
||||
$doSave = new \ReflectionMethod($adapter, 'doSave');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$doSave->invoke($adapter, ['key' => 'value'], 0);
|
||||
}
|
||||
|
||||
protected function isPruned(DoctrineDbalAdapter $cache, string $name): bool
|
||||
{
|
||||
$o = new \ReflectionObject($cache);
|
||||
|
||||
Reference in New Issue
Block a user