feature #24781 [HttpFoundation] RedisSessionHandler (dkarlovi)
This PR was merged into the 4.1-dev branch.
Discussion
----------
[HttpFoundation] RedisSessionHandler
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #24433, #18233, #14539, #4538, #3498
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/pull/8572
Ability to use Redis as a session storage backend. Discussed in detail in linked issues / PRs.
Commits
-------
8776cce
[HttpFoundation] Add RedisSessionHandler
This commit is contained in:
commit
78ebc6d5ce
@ -8,6 +8,7 @@ CHANGELOG
|
||||
supported anymore in 5.0.
|
||||
|
||||
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
|
||||
* added `RedisSessionHandler` to use Redis as a session storage
|
||||
|
||||
4.0.0
|
||||
-----
|
||||
|
@ -0,0 +1,105 @@
|
||||
<?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;
|
||||
|
||||
use Predis\Response\ErrorInterface;
|
||||
|
||||
/**
|
||||
* Redis based session storage handler based on the Redis class
|
||||
* provided by the PHP redis extension.
|
||||
*
|
||||
* @author Dalibor Karlović <dalibor@flexolabs.io>
|
||||
*/
|
||||
class RedisSessionHandler extends AbstractSessionHandler
|
||||
{
|
||||
private $redis;
|
||||
|
||||
/**
|
||||
* @var string Key prefix for shared environments
|
||||
*/
|
||||
private $prefix;
|
||||
|
||||
/**
|
||||
* List of available options:
|
||||
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server.
|
||||
*
|
||||
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redis
|
||||
* @param array $options An associative array of options
|
||||
*
|
||||
* @throws \InvalidArgumentException When unsupported client or options are passed
|
||||
*/
|
||||
public function __construct($redis, array $options = array())
|
||||
{
|
||||
if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \Predis\Client && !$redis instanceof RedisProxy) {
|
||||
throw new \InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redis) ? get_class($redis) : gettype($redis)));
|
||||
}
|
||||
|
||||
if ($diff = array_diff(array_keys($options), array('prefix'))) {
|
||||
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff)));
|
||||
}
|
||||
|
||||
$this->redis = $redis;
|
||||
$this->prefix = $options['prefix'] ?? 'sf_s';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doRead($sessionId): string
|
||||
{
|
||||
return $this->redis->get($this->prefix.$sessionId) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doWrite($sessionId, $data): bool
|
||||
{
|
||||
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'), $data);
|
||||
|
||||
return $result && !$result instanceof ErrorInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function doDestroy($sessionId): bool
|
||||
{
|
||||
$this->redis->del($this->prefix.$sessionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function close(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function gc($maxlifetime): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function updateTimestamp($sessionId, $data)
|
||||
{
|
||||
return $this->redis->expire($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'));
|
||||
}
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
<?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\RedisSessionHandler;
|
||||
|
||||
/**
|
||||
* @requires extension redis
|
||||
* @group time-sensitive
|
||||
*/
|
||||
abstract class AbstractRedisSessionHandlerTestCase extends TestCase
|
||||
{
|
||||
protected const PREFIX = 'prefix_';
|
||||
|
||||
/**
|
||||
* @var RedisSessionHandler
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* @var \Redis|\RedisArray|\RedisCluster|\Predis\Client
|
||||
*/
|
||||
protected $redisClient;
|
||||
|
||||
/**
|
||||
* @var \Redis
|
||||
*/
|
||||
protected $validator;
|
||||
|
||||
/**
|
||||
* @return \Redis|\RedisArray|\RedisCluster|\Predis\Client
|
||||
*/
|
||||
abstract protected function createRedisClient(string $host);
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (!extension_loaded('redis')) {
|
||||
self::markTestSkipped('Extension redis required.');
|
||||
}
|
||||
|
||||
$host = getenv('REDIS_HOST') ?: 'localhost';
|
||||
|
||||
$this->validator = new \Redis();
|
||||
$this->validator->connect($host);
|
||||
|
||||
$this->redisClient = $this->createRedisClient($host);
|
||||
$this->storage = new RedisSessionHandler(
|
||||
$this->redisClient,
|
||||
array('prefix' => self::PREFIX)
|
||||
);
|
||||
}
|
||||
|
||||
protected function tearDown()
|
||||
{
|
||||
$this->redisClient = 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->setFixture(self::PREFIX.'id1', null);
|
||||
$this->setFixture(self::PREFIX.'id2', 'abc123');
|
||||
|
||||
$this->assertEquals('', $this->storage->read('id1'));
|
||||
$this->assertEquals('abc123', $this->storage->read('id2'));
|
||||
}
|
||||
|
||||
public function testWriteSession()
|
||||
{
|
||||
$this->assertTrue($this->storage->write('id', 'data'));
|
||||
|
||||
$this->assertTrue($this->hasFixture(self::PREFIX.'id'));
|
||||
$this->assertEquals('data', $this->getFixture(self::PREFIX.'id'));
|
||||
}
|
||||
|
||||
public function testUseSessionGcMaxLifetimeAsTimeToLive()
|
||||
{
|
||||
$this->storage->write('id', 'data');
|
||||
$ttl = $this->fixtureTtl(self::PREFIX.'id');
|
||||
|
||||
$this->assertLessThanOrEqual(ini_get('session.gc_maxlifetime'), $ttl);
|
||||
$this->assertGreaterThanOrEqual(0, $ttl);
|
||||
}
|
||||
|
||||
public function testDestroySession()
|
||||
{
|
||||
$this->setFixture(self::PREFIX.'id', 'foo');
|
||||
|
||||
$this->assertTrue($this->hasFixture(self::PREFIX.'id'));
|
||||
$this->assertTrue($this->storage->destroy('id'));
|
||||
$this->assertFalse($this->hasFixture(self::PREFIX.'id'));
|
||||
}
|
||||
|
||||
public function testGcSession()
|
||||
{
|
||||
$this->assertTrue($this->storage->gc(123));
|
||||
}
|
||||
|
||||
public function testUpdateTimestamp()
|
||||
{
|
||||
$lowTTL = 10;
|
||||
|
||||
$this->setFixture(self::PREFIX.'id', 'foo', $lowTTL);
|
||||
$this->storage->updateTimestamp('id', array());
|
||||
|
||||
$this->assertGreaterThan($lowTTL, $this->fixtureTtl(self::PREFIX.'id'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getOptionFixtures
|
||||
*/
|
||||
public function testSupportedParam(array $options, bool $supported)
|
||||
{
|
||||
try {
|
||||
new RedisSessionHandler($this->redisClient, $options);
|
||||
$this->assertTrue($supported);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->assertFalse($supported);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOptionFixtures(): array
|
||||
{
|
||||
return array(
|
||||
array(array('prefix' => 'session'), true),
|
||||
array(array('prefix' => 'sfs', 'foo' => 'bar'), false),
|
||||
);
|
||||
}
|
||||
|
||||
protected function setFixture($key, $value, $ttl = null)
|
||||
{
|
||||
if (null !== $ttl) {
|
||||
$this->validator->setex($key, $ttl, $value);
|
||||
} else {
|
||||
$this->validator->set($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getFixture($key)
|
||||
{
|
||||
return $this->validator->get($key);
|
||||
}
|
||||
|
||||
protected function hasFixture($key): bool
|
||||
{
|
||||
return $this->validator->exists($key);
|
||||
}
|
||||
|
||||
protected function fixtureTtl($key): int
|
||||
{
|
||||
return $this->validator->ttl($key);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?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 Predis\Client;
|
||||
|
||||
class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
|
||||
{
|
||||
protected function createRedisClient(string $host): Client
|
||||
{
|
||||
return new Client(array(array('host' => $host)));
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?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 Predis\Client;
|
||||
|
||||
class PredisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
|
||||
{
|
||||
protected function createRedisClient(string $host): Client
|
||||
{
|
||||
return new Client(array('host' => $host));
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
|
||||
class RedisArraySessionHandlerTest extends AbstractRedisSessionHandlerTestCase
|
||||
{
|
||||
protected function createRedisClient(string $host): \RedisArray
|
||||
{
|
||||
return new \RedisArray(array($host));
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?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;
|
||||
|
||||
class RedisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
|
||||
{
|
||||
protected function createRedisClient(string $host): \Redis
|
||||
{
|
||||
$client = new \Redis();
|
||||
$client->connect($host);
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user