From 0286208db17e620fa84d8cdbfcb19af1e7c9fa94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Karlovi=C4=87?= Date: Wed, 1 Nov 2017 11:36:27 +0100 Subject: [PATCH] [HttpFoundation] Add RedisSessionHandler --- .../Component/HttpFoundation/CHANGELOG.md | 1 + .../Storage/Handler/RedisSessionHandler.php | 116 ++++++++++++++++ .../Handler/RedisSessionHandlerTest.php | 124 ++++++++++++++++++ 3 files changed, 241 insertions(+) create mode 100644 src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php create mode 100644 src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php diff --git a/src/Symfony/Component/HttpFoundation/CHANGELOG.md b/src/Symfony/Component/HttpFoundation/CHANGELOG.md index ee5b6cecf2e85..ac0d500083a1a 100644 --- a/src/Symfony/Component/HttpFoundation/CHANGELOG.md +++ b/src/Symfony/Component/HttpFoundation/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()` * deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead * deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead + * added `RedisSessionHandler` to use Redis as a session storage 3.3.0 ----- diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php new file mode 100644 index 0000000000000..f138ad2d5513d --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/RedisSessionHandler.php @@ -0,0 +1,116 @@ + + * + * 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; + +/** + * Redis based session storage handler based on the Redis class + * provided by the PHP redis extension. + * + * Direct port of MemcachedSessionHandler to Redis. + * + * @see http://php.net/redis + * + * @author Drak + * @author Dalibor Karlović + */ +class RedisSessionHandler extends AbstractSessionHandler +{ + /** + * @var \Redis + */ + private $redis; + + /** + * @var int Time to live in seconds + */ + private $ttl; + + /** + * @var string Key prefix for shared environments + */ + private $prefix; + + /** + * List of available options: + * * prefix: The prefix to use for the Redis keys in order to avoid collision + * * expiretime: The time to live in seconds. + * + * @param \Redis $redis A \Redis instance + * @param array $options An associative array of Redis options + * + * @throws \InvalidArgumentException When unsupported options are passed + */ + public function __construct(\Redis $redis, array $options = null) + { + $this->redis = $redis; + + if ($diff = array_diff(array_keys($options), array('prefix', 'expiretime'))) { + throw new \InvalidArgumentException(sprintf( + 'The following options are not supported "%s"', implode(', ', $diff) + )); + } + + $this->ttl = isset($options['expiretime']) ? (int) $options['expiretime'] : 86400; + $this->prefix = isset($options['prefix']) ? $options['prefix'] : 'sf2s'; + } + + /** + * {@inheritdoc} + */ + protected function doRead($sessionId) + { + return $this->redis->get($this->prefix.$sessionId) ?: ''; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($sessionId, $data) + { + return $this->redis->set($this->prefix.$sessionId, $data, $this->ttl); + } + + /** + * {@inheritdoc} + */ + protected function doDestroy($sessionId) + { + // number of deleted items + $count = $this->redis->del($this->prefix.$sessionId); + + return 1 === $count; + } + + /** + * {@inheritdoc} + */ + public function close() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function gc($maxlifetime) + { + return true; + } + + /** + * {@inheritdoc} + */ + public function updateTimestamp($sessionId, $data) + { + return $this->redis->expire($this->prefix.$sessionId, $this->ttl); + } +} diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php new file mode 100644 index 0000000000000..d657d2cb7c691 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/RedisSessionHandlerTest.php @@ -0,0 +1,124 @@ + + * + * 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\RedisSessionHandler; + +/** + * @requires extension redis + * @group time-sensitive + */ +class RedisSessionHandlerTest extends TestCase +{ + const PREFIX = 'prefix_'; + const TTL = 1000; + + /** + * @var RedisSessionHandler + */ + protected $storage; + + /** @var \PHPUnit_Framework_MockObject_MockObject|\Redis $redis */ + protected $redis; + + protected function setUp() + { + parent::setUp(); + + $this->redis = $this->getMockBuilder('Redis')->getMock(); + $this->storage = new RedisSessionHandler( + $this->redis, + array('prefix' => self::PREFIX, 'expiretime' => self::TTL) + ); + } + + protected function tearDown() + { + $this->redis = null; + $this->storage = null; + parent::tearDown(); + } + + public function testOpenSession() + { + $this->assertTrue($this->storage->open('', '')); + } + + public function testCloseSession() + { + $this->assertTrue($this->storage->close()); + } + + public function testReadSession() + { + $this->redis + ->expects($this->once()) + ->method('get') + ->with(self::PREFIX.'id') + ; + + $this->assertEquals('', $this->storage->read('id')); + } + + public function testWriteSession() + { + $this->redis + ->expects($this->once()) + ->method('set') + ->with(self::PREFIX.'id', 'data', self::TTL) + ->will($this->returnValue(true)) + ; + + $this->assertTrue($this->storage->write('id', 'data')); + } + + public function testDestroySession() + { + $this->redis + ->expects($this->once()) + ->method('del') + ->with(self::PREFIX.'id') + ->will($this->returnValue(1)) + ; + + $this->assertTrue($this->storage->destroy('id')); + } + + public function testGcSession() + { + $this->assertTrue($this->storage->gc(123)); + } + + /** + * @dataProvider getOptionFixtures + */ + public function testSupportedOptions(array $options, $supported) + { + try { + new RedisSessionHandler($this->redis, $options); + $this->assertTrue($supported); + } catch (\InvalidArgumentException $e) { + $this->assertFalse($supported); + } + } + + public function getOptionFixtures() + { + return array( + array(array('prefix' => 'session'), true), + array(array('expiretime' => 100), true), + array(array('prefix' => 'session', 'expiretime' => 200), true), + array(array('expiretime' => 100, 'foo' => 'bar'), false), + ); + } +}