feature #26096 [HttpFoundation] Added a migrating session handler (rossmotley)
This PR was squashed before being merged into the 4.1-dev branch (closes #26096).
Discussion
----------
[HttpFoundation] Added a migrating session handler
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets |
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/9496
- [x] gather feedback for my changes
- [x] submit changes to the documentation
- [x] update the changelog
When migrating to a new session handler on a live system, it's useful to be able to do it with no loss of session data. This migrating handler allows the sessions to be written to an additional handler to enable a migration workflow like:
* Switch to migrating handler, with your new handler as the 'write only' one. The old handler behaves as usual and sessions get written to the new one.
* After verifying the data in the new handler (and after the session gc period), switch the migrating handler to use your old handler as the 'write only' one instead, so the sessions will now be read from the new handler. This step allows easier rollbacks.
* After verifying everything, switch from the migrating handler to the new handler
Commits
-------
3acd548349
[HttpFoundation] Added a migrating session handler
This commit is contained in:
commit
0f4c0e92b2
@ -15,6 +15,7 @@ CHANGELOG
|
||||
* added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`,
|
||||
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to
|
||||
handle failed `UploadedFile`.
|
||||
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
|
||||
|
||||
/**
|
||||
* Migrating session handler for migrating from one handler to another. It reads
|
||||
* from the current handler and writes both the current and new ones.
|
||||
*
|
||||
* It ignores errors from the new handler.
|
||||
*
|
||||
* @author Ross Motley <ross.motley@amara.com>
|
||||
* @author Oliver Radwell <oliver.radwell@amara.com>
|
||||
*/
|
||||
class MigratingSessionHandler implements \SessionHandlerInterface
|
||||
{
|
||||
private $currentHandler;
|
||||
private $writeOnlyHandler;
|
||||
|
||||
public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler)
|
||||
{
|
||||
$this->currentHandler = $currentHandler;
|
||||
$this->writeOnlyHandler = $writeOnlyHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
$result = $this->currentHandler->close();
|
||||
$this->writeOnlyHandler->close();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function destroy($sessionId)
|
||||
{
|
||||
$result = $this->currentHandler->destroy($sessionId);
|
||||
$this->writeOnlyHandler->destroy($sessionId);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function gc($maxlifetime)
|
||||
{
|
||||
$result = $this->currentHandler->gc($maxlifetime);
|
||||
$this->writeOnlyHandler->gc($maxlifetime);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function open($savePath, $sessionId)
|
||||
{
|
||||
$result = $this->currentHandler->open($savePath, $sessionId);
|
||||
$this->writeOnlyHandler->open($savePath, $sessionId);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function read($sessionId)
|
||||
{
|
||||
// No reading from new handler until switch-over
|
||||
return $this->currentHandler->read($sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function write($sessionId, $sessionData)
|
||||
{
|
||||
$result = $this->currentHandler->write($sessionId, $sessionData);
|
||||
$this->writeOnlyHandler->write($sessionId, $sessionData);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\MigratingSessionHandler;
|
||||
|
||||
class MigratingSessionHandlerTest extends TestCase
|
||||
{
|
||||
private $dualHandler;
|
||||
private $currentHandler;
|
||||
private $writeOnlyHandler;
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
$this->currentHandler = $this->createMock(\SessionHandlerInterface::class);
|
||||
$this->writeOnlyHandler = $this->createMock(\SessionHandlerInterface::class);
|
||||
|
||||
$this->dualHandler = new MigratingSessionHandler($this->currentHandler, $this->writeOnlyHandler);
|
||||
}
|
||||
|
||||
public function testCloses()
|
||||
{
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('close')
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->once())
|
||||
->method('close')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$result = $this->dualHandler->close();
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testDestroys()
|
||||
{
|
||||
$sessionId = 'xyz';
|
||||
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('destroy')
|
||||
->with($sessionId)
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->once())
|
||||
->method('destroy')
|
||||
->with($sessionId)
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$result = $this->dualHandler->destroy($sessionId);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testGc()
|
||||
{
|
||||
$maxlifetime = 357;
|
||||
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('gc')
|
||||
->with($maxlifetime)
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->once())
|
||||
->method('gc')
|
||||
->with($maxlifetime)
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$result = $this->dualHandler->gc($maxlifetime);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testOpens()
|
||||
{
|
||||
$savePath = '/path/to/save/location';
|
||||
$sessionId = 'xyz';
|
||||
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('open')
|
||||
->with($savePath, $sessionId)
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->once())
|
||||
->method('open')
|
||||
->with($savePath, $sessionId)
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$result = $this->dualHandler->open($savePath, $sessionId);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testReads()
|
||||
{
|
||||
$sessionId = 'xyz';
|
||||
$readValue = 'something';
|
||||
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('read')
|
||||
->with($sessionId)
|
||||
->will($this->returnValue($readValue));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->never())
|
||||
->method('read')
|
||||
->with($this->any());
|
||||
|
||||
$result = $this->dualHandler->read($sessionId);
|
||||
|
||||
$this->assertEquals($readValue, $result);
|
||||
}
|
||||
|
||||
public function testWrites()
|
||||
{
|
||||
$sessionId = 'xyz';
|
||||
$data = 'my-serialized-data';
|
||||
|
||||
$this->currentHandler->expects($this->once())
|
||||
->method('write')
|
||||
->with($sessionId, $data)
|
||||
->will($this->returnValue(true));
|
||||
|
||||
$this->writeOnlyHandler->expects($this->once())
|
||||
->method('write')
|
||||
->with($sessionId, $data)
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$result = $this->dualHandler->write($sessionId, $data);
|
||||
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user