Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -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'));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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)));
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}