Skip to content

Commit 91c8c34

Browse files
committed
Added Redis as possible session storage mechanism, fixes #574
1 parent 30bdd54 commit 91c8c34

5 files changed

Lines changed: 304 additions & 0 deletions

File tree

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
"phpunit/phpunit": "~4.8",
4848
"satooshi/php-coveralls": "^1.0"
4949
},
50+
"suggests": {
51+
"predis/predis": "1.1.1"
52+
},
5053
"support": {
5154
"issues": "https://github.com/simplesamlphp/simplesamlphp/issues",
5255
"source": "https://github.com/simplesamlphp/simplesamlphp"

config-templates/config.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@
975975
* - 'phpsession': Limited datastore, which uses the PHP session.
976976
* - 'memcache': Key-value datastore, based on memcache.
977977
* - 'sql': SQL datastore, using PDO.
978+
* - 'redis': Key-value datastore, based on redis.
978979
*
979980
* The default datastore is 'phpsession'.
980981
*
@@ -1000,4 +1001,15 @@
10001001
* The prefix we should use on our tables.
10011002
*/
10021003
'store.sql.prefix' => 'SimpleSAMLphp',
1004+
1005+
/*
1006+
* The hostname and port of the Redis datastore instance.
1007+
*/
1008+
'store.redis.host' => 'localhost',
1009+
'store.redis.port' => 6379,
1010+
1011+
/*
1012+
* The prefix we should use on our Redis datastore.
1013+
*/
1014+
'store.redis.prefix' => 'simpleSAMLphp',
10031015
);

lib/SimpleSAML/Store.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ public static function getInstance()
5151
case 'sql':
5252
self::$instance = new Store\SQL();
5353
break;
54+
case 'redis':
55+
self::$instance = new Store\Redis();
56+
break;
5457
default:
5558
// datastore from module
5659
try {

lib/SimpleSAML/Store/Redis.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace SimpleSAML\Store;
4+
5+
use \SimpleSAML_Configuration as Configuration;
6+
use \SimpleSAML\Store;
7+
8+
/**
9+
* A data store using Redis to keep the data.
10+
*
11+
* @package SimpleSAMLphp
12+
*/
13+
class Redis extends Store
14+
{
15+
/**
16+
* Initialize the Redis data store.
17+
*/
18+
public function __construct($redis = null)
19+
{
20+
assert('is_null($redis) || is_subclass_of($redis, "Predis\\Client")');
21+
22+
if (is_null($redis)) {
23+
$config = Configuration::getInstance();
24+
25+
$host = $config->getString('store.redis.host', 'localhost');
26+
$port = $config->getInteger('store.redis.port', 6379);
27+
$prefix = $config->getString('store.redis.prefix', 'simpleSAMLphp');
28+
29+
$redis = new \Predis\Client(
30+
array(
31+
'scheme' => 'tcp',
32+
'host' => $host,
33+
'post' => $port,
34+
),
35+
array(
36+
'prefix' => $prefix,
37+
)
38+
);
39+
}
40+
41+
$this->redis = $redis;
42+
}
43+
44+
/**
45+
* Deconstruct the Redis data store.
46+
*/
47+
public function __destruct()
48+
{
49+
if (method_exists($this->redis, 'disconnect')) {
50+
$this->redis->disconnect();
51+
}
52+
}
53+
54+
/**
55+
* Retrieve a value from the data store.
56+
*
57+
* @param string $type The type of the data.
58+
* @param string $key The key to retrieve.
59+
*
60+
* @return mixed|null The value associated with that key, or null if there's no such key.
61+
*/
62+
public function get($type, $key)
63+
{
64+
assert('is_string($type)');
65+
assert('is_string($key)');
66+
67+
$result = $this->redis->get("{$type}.{$key}");
68+
69+
if ($result === false) {
70+
return null;
71+
}
72+
73+
return unserialize($result);
74+
}
75+
76+
/**
77+
* Save a value in the data store.
78+
*
79+
* @param string $type The type of the data.
80+
* @param string $key The key to insert.
81+
* @param mixed $value The value itself.
82+
* @param int|null $expire The expiration time (unix timestamp), or null if it never expires.
83+
*/
84+
public function set($type, $key, $value, $expire = null)
85+
{
86+
assert('is_string($type)');
87+
assert('is_string($key)');
88+
assert('is_null($expire) || (is_int($expire) && $expire > 2592000)');
89+
90+
$serialized = serialize($value);
91+
92+
if (is_null($expire)) {
93+
$this->redis->set("{$type}.{$key}", $serialized);
94+
} else {
95+
$this->redis->setex("{$type}.{$key}", $expire, $serialized);
96+
}
97+
}
98+
99+
/**
100+
* Delete an entry from the data store.
101+
*
102+
* @param string $type The type of the data
103+
* @param string $key The key to delete.
104+
*/
105+
public function delete($type, $key)
106+
{
107+
assert('is_string($type)');
108+
assert('is_string($key)');
109+
110+
$this->redis->del("{$type}.{$key}");
111+
}
112+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
namespace SimpleSAML\Test\Store;
4+
5+
use \SimpleSAML_Configuration as Configuration;
6+
use \SimpleSAML\Store;
7+
8+
/**
9+
* Tests for the Redis store.
10+
*
11+
* For the full copyright and license information, please view the LICENSE file that was distributed with this source
12+
* code.
13+
*
14+
* @package simplesamlphp/simplesamlphp
15+
*/
16+
class RedisTest extends \PHPUnit_Framework_TestCase
17+
{
18+
protected function setUp()
19+
{
20+
$this->config = array();
21+
22+
$this->mocked_redis = $this->getMockBuilder('Predis\Client')
23+
->setMethods(array('get', 'set', 'setex', 'del', 'disconnect'))
24+
->disableOriginalConstructor()
25+
->getMock();
26+
27+
$this->mocked_redis->method('get')
28+
->will($this->returnCallback(array($this, 'getMocked')));
29+
30+
$this->mocked_redis->method('set')
31+
->will($this->returnCallback(array($this, 'setMocked')));
32+
33+
$this->mocked_redis->method('setex')
34+
->will($this->returnCallback(array($this, 'setexMocked')));
35+
36+
$this->mocked_redis->method('del')
37+
->will($this->returnCallback(array($this, 'delMocked')));
38+
39+
$nop = function () {
40+
return;
41+
};
42+
43+
$this->mocked_redis->method('disconnect')
44+
->will($this->returnCallback($nop));
45+
46+
$this->redis = new Store\Redis($this->mocked_redis);
47+
}
48+
49+
public function getMocked($key)
50+
{
51+
return array_key_exists($key, $this->config) ? $this->config[$key] : false;
52+
}
53+
54+
public function setMocked($key, $value)
55+
{
56+
$this->config[$key] = $value;
57+
}
58+
59+
public function setexMocked($key, $expire, $value)
60+
{
61+
// Testing expiring data is more trouble than it's worth for now
62+
$this->setMocked($key, $value);
63+
}
64+
65+
public function delMocked($key)
66+
{
67+
unset($this->config[$key]);
68+
}
69+
70+
/**
71+
* @covers \SimpleSAML\Store::getInstance
72+
* @covers \SimpleSAML\Store\Redis::__construct
73+
* @test
74+
*/
75+
public function testRedisInstance()
76+
{
77+
$config = Configuration::loadFromArray(array(
78+
'store.type' => 'redis',
79+
'store.redis.prefix' => 'phpunit_',
80+
), '[ARRAY]', 'simplesaml');
81+
82+
$store = Store::getInstance();
83+
84+
$this->assertInstanceOf('SimpleSAML\Store\Redis', $store);
85+
86+
$this->clearInstance($config, '\SimpleSAML_Configuration');
87+
$this->clearInstance($store, '\SimpleSAML\Store');
88+
}
89+
90+
/**
91+
* @covers \SimpleSAML\Store\Redis::get
92+
* @covers \SimpleSAML\Store\Redis::set
93+
* @test
94+
*/
95+
public function testInsertData()
96+
{
97+
$value = 'TEST';
98+
99+
$this->redis->set('test', 'key', $value);
100+
$res = $this->redis->get('test', 'key');
101+
$expected = $value;
102+
103+
$this->assertEquals($expected, $res);
104+
}
105+
106+
/**
107+
* @covers \SimpleSAML\Store\Redis::get
108+
* @covers \SimpleSAML\Store\Redis::set
109+
* @test
110+
*/
111+
public function testInsertExpiringData()
112+
{
113+
$value = 'TEST';
114+
115+
$this->redis->set('test', 'key', $value, $expire = 80808080);
116+
$res = $this->redis->get('test', 'key');
117+
$expected = $value;
118+
119+
$this->assertEquals($expected, $res);
120+
}
121+
122+
/**
123+
* @covers \SimpleSAML\Store\Redis::get
124+
* @test
125+
*/
126+
public function testGetEmptyData()
127+
{
128+
$res = $this->redis->get('test', 'key');
129+
130+
$this->assertNull($res);
131+
}
132+
133+
/**
134+
* @covers \SimpleSAML\Store\Redis::get
135+
* @covers \SimpleSAML\Store\Redis::set
136+
* @test
137+
*/
138+
public function testOverwriteData()
139+
{
140+
$value1 = 'TEST1';
141+
$value2 = 'TEST2';
142+
143+
$this->redis->set('test', 'key', $value1);
144+
$this->redis->set('test', 'key', $value2);
145+
$res = $this->redis->get('test', 'key');
146+
$expected = $value2;
147+
148+
$this->assertEquals($expected, $res);
149+
}
150+
151+
/**
152+
* @covers \SimpleSAML\Store\Redis::get
153+
* @covers \SimpleSAML\Store\Redis::set
154+
* @covers \SimpleSAML\Store\Redis::delete
155+
* @test
156+
*/
157+
public function testDeleteData()
158+
{
159+
$this->redis->set('test', 'key', 'TEST');
160+
$this->redis->delete('test', 'key');
161+
$res = $this->redis->get('test', 'key');
162+
163+
$this->assertNull($res);
164+
}
165+
166+
protected function clearInstance($service, $className)
167+
{
168+
$reflectedClass = new \ReflectionClass($className);
169+
$reflectedInstance = $reflectedClass->getProperty('instance');
170+
$reflectedInstance->setAccessible(true);
171+
$reflectedInstance->setValue($service, null);
172+
$reflectedInstance->setAccessible(false);
173+
}
174+
}

0 commit comments

Comments
 (0)