10000 [HttpFoundation] Add RedisSessionHandler · symfony/symfony@8776cce · GitHub
[go: up one dir, main page]

Skip to content

Commit 8776cce

Browse files
dkarlovinicolas-grekas
authored andcommitted
[HttpFoundation] Add RedisSessionHandler
1 parent 2ef0d60 commit 8776cce

File tree

7 files changed

+370
-0
lines changed

7 files changed

+370
-0
lines changed

src/Symfony/Component/HttpFoundation/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
supported anymore in 5.0.
99

1010
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
11+
* added `RedisSessionHandler` to use Redis as a session storage
1112

1213
4.0.0
1314
-----
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
13+
14+
use Predis\Response\ErrorInterface;
15+
16+
/**
17+
* Redis based session storage handler based on the Redis class
18+
* provided by the PHP redis extension.
19+
*
20+
* @author Dalibor Karlović <dalibor@flexolabs.io>
21+
*/
22+
class RedisSessionHandler extends AbstractSessionHandler
23+
{
24+
private $redis;
25+
26+
/**
27+
* @var string Key prefix for shared environments
28+
*/
29+
private $prefix;
30+
31+
/**
32+
* List of available options:
33+
* * prefix: The prefix to use for the keys in order to avoid collision on the Redis server.
34+
*
35+
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redis
36+
* @param array $options An associative array of options
37+
*
38+
* @throws \InvalidArgumentException When unsupported client or options are passed
39+
*/
40+
public function __construct($redis, array $options = array())
41+
{
42+
if (!$redis instanceof \Redis && !$redis instanceof \RedisArray && !$redis instanceof \Predis\Client && !$redis instanceof RedisProxy) {
43+
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)));
44+
}
45+
46+
if ($diff = array_diff(array_keys($options), array('prefix'))) {
47+
throw new \InvalidArgumentException(sprintf('The following options are not supported "%s"', implode(', ', $diff)));
48+
}
49+
50+
$this->redis = $redis;
51+
$this->prefix = $options['prefix'] ?? 'sf_s';
52+
}
53+
54+
/**
55+
* {@inheritdoc}
56+
*/
57+
protected function doRead($sessionId): string
58+
{
59+
return $this->redis->get($this->prefix.$sessionId) ?: '';
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
protected function doWrite($sessionId, $data): bool
66+
{
67+
$result = $this->redis->setEx($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'), $data);
68+
69+
return $result && !$result instanceof ErrorInterface;
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
protected function doDestroy($sessionId): bool
76+
{
77+
$this->redis->del($this->prefix.$sessionId);
78+
79+
return true;
80+
}
81+
82+
/**
83+
* {@inheritdoc}
84+
*/
85+
public function close(): bool
86+
{
87+
return true;
88+
}
89+
90+
/**
91+
* {@inheritdoc}
92+
*/
93+
public function gc($maxlifetime): bool
94+
{
95+
return true;
96+
}
97+
98+
/**
99+
* {@inheritdoc}
100+
*/
101+
public function updateTimestamp($sessionId, $data)
102+
{
103+
return $this->redis->expire($this->prefix.$sessionId, (int) ini_get('session.gc_maxlifetime'));
104+
}
105+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler;
16+
17+
/**
18+
* @requires extension redis
19+
* @group time-sensitive
20+
*/
21+
abstract class AbstractRedisSessionHandlerTestCase extends TestCase
22+
{
23+
protected const PREFIX = 'prefix_';
24+
25+
/**
26+
* @var RedisSessionHandler
27+
*/
28+
protected $storage;
29+
30+
/**
31+
* @var \Redis|\RedisArray|\RedisCluster|\Predis\Client
32+
*/
33+
protected $redisClient;
34+
35+
/**
36+
* @var \Redis
37+
*/
38+
protected $validator;
39+
40+
/**
41+
* @return \Redis|\RedisArray|\RedisCluster|\Predis\Client
42+
*/
43+
abstract protected function createRedisClient(string $host);
44+
45+
protected function setUp()
46+
{
47+
parent::setUp();
48+
49+
if (!extension_loaded('redis')) {
50+
self::markTestSkipped('Extension redis required.');
51+
}
52+
53+
$host = getenv('REDIS_HOST') ?: 'localhost';
54+
55+
$this->validator = new \Redis();
56+
$this->validator->connect($host);
57+
58+
$this->redisClient = $this->createRedisClient($host);
59+
$this->storage = new RedisSessionHandler(
60+
$this->redisClient,
61+
array('prefix' => self::PREFIX)
62+
);
63+
}
64+
65+
protected function tearDown()
66+
{
67+
$this->redisClient = null;
68+
$this->storage = null;
69+
70+
parent::tearDown();
71+
}
72+
73+
public function testOpenSession()
74+
{
75+
$this->assertTrue($this->storage->open('', ''));
76+
}
77+
78+
public function testCloseSession()
79+
{
80+
$this->assertTrue($this->storage->close());
81+
}
82+
83+
public function testReadSession()
84+
{
85+
$this->setFixture(self::PREFIX.'id1', null);
86+
$this->setFixture(self::PREFIX.'id2', 'abc123');
87+
88+
$this->assertEquals('', $this->storage->read('id1'));
89+
$this->assertEquals('abc123', $this->storage->read('id2'));
90+
}
91+
92+
public function testWriteSession()
93+
{
94+
$this->assertTrue($this->storage->write('id', 'data'));
95+
96+
$this->assertTrue($this->hasFixture(self::PREFIX.'id'));
97+
$this->assertEquals('data', $this->getFixture(self::PREFIX.'id'));
98+
}
99+
100+
public function testUseSessionGcMaxLifetimeAsTimeToLive()
101+
{
102+
$this->storage->write('id', 'data');
103+
$ttl = $this->fixtureTtl(self::PREFIX.'id');
104+
105+
$this->assertLessThanOrEqual(ini_get('session.gc_maxlifetime'), $ttl);
106+
$this->assertGreaterThanOrEqual(0, $ttl);
107+
}
108+
109+
public function testDestroySession()
110+
{
111+
$this->setFixture(self::PREFIX.'id', 'foo');
112+
113+
$this->assertTrue($this->hasFixture(self::PREFIX.'id'));
114+
$this->assertTrue($this->storage->destroy('id'));
115+
$this->assertFalse($this->hasFixture(self::PREFIX.'id'));
116+
}
117+
118+
public function testGcSession()
119+
{
120+
$this->assertTrue($this->storage->gc(123));
121+
}
122+
123+
public function testUpdateTimestamp()
124+
{
125+
$lowTTL = 10;
126+
127+
$this->setFixture(self::PREFIX.'id', 'foo', $lowTTL);
128+
$this->storage->updateTimestamp('id', array());
129+
130+
$this->assertGreaterThan($lowTTL, $this->fixtureTtl(self::PREFIX.'id'));
131+
}
132+
133+
/**
134+
* @dataProvider getOptionFixtures
135+
*/
136+
public function testSupportedParam(array $options, bool $supported)
137+
{
138+
try {
139+
new RedisSessionHandler($this->redisClient, $options);
140+
$this->assertTrue($supported);
141+
} catch (\InvalidArgumentException $e) {
142+
$this->assertFalse($supported);
143+
}
144+
}
145+
146+
public function getOptionFixtures(): array
147+
{
148+
return array(
149+
array(array('prefix' => 'session'), true),
150+
array(array('prefix' => 'sfs', 'foo' => 'bar'), false),
151+
);
152+
}
153+
154+
protected function setFixture($key, $value, $ttl = null)
155+
{
156+
if (null !== $ttl) {
157+
$this->validator->setex($key, $ttl, $value);
158+
} else {
159+
$this->validator->set($key, $value);
160+
}
161+
}
162+
163+
protected function getFixture($key)
164+
{
165+
return $this->validator->get($key);
166+
}
167+
168+
protected function hasFixture($key): bool
169+
{
170+
return $this->validator->exists($key);
171+
}
172+
173+
protected function fixtureTtl($key): int
174+
{
175+
return $this->validator->ttl($key);
176+
}
177+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
13+
14+
use Predis\Client;
15+
16+
class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
17+
{
18+
protected function createRedisClient(string $host): Client
19+
{
20+
return new Client(array(array('host' => $host)));
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
13+
14+
use Predis\Client;
15+
16+
class PredisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
17+
{
18+
protected function createRedisClient(string $host): Client
19+
{
20+
return new Client(array('host' => $host));
21+
}
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
13+
14+
class RedisArraySessionHandlerTest extends AbstractRedisSessionHandlerTestCase
15+
{
16+
protected function createRedisClient(string $host): \RedisArray
17+
{
18+
return new \RedisArray(array($host));
19+
}
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
13+
14+
class RedisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
15+
{
16+
protected function createRedisClient(string $host): \Redis
17+
{
18+
$client = new \Redis();
19+
$client->connect($host);
20+
21+
return $client;
22+
}
23+
}

0 commit comments

Comments
 (0)
0